python面向對象

面向對象是一種編程範式。範式,能夠認爲是一組方法論,編程範式是一組如何組織代碼的方法論。主流的編程範式有:PP、IP、FP、LP、OOP、AOP。java

  • PP:面向過程,表明是 C 語言;
  • IP:面向指令,表明是彙編;
  • FP:函數式編程,把世界抽象成一個個的函數,它要求的是無反作用,即相同的輸入會產生相同的輸出。所以它是一種無反作用的過程;
  • LP:面向邏輯的編程,把世界抽象成與或非,表明是 prolog;
  • AOP:面向方面,它用來解決一類問題,Python 中的裝飾器就是 AOP 的思想。表明是 SQL 語句;

OOP 就是今天的主角,它的世界觀爲:node

  • 世界由對象組成;
  • 對象具備運動規律和內部狀態;
  • 對象之間能夠相互做用。

以目前人類的認知來講,OOP 是最接近真實世界的編程範式。python

設計大型軟件時,面向對象比面向過程更容易實現。程序由指令加數據組成,代碼能夠選擇以指令爲核心或以數據爲核心進行編寫:程序員

  • 以指令爲核心:圍繞「正在發生什麼」進行編寫。這是面向過程編程,程序具備一系列線性步驟,主體思想是代碼做用於數據;
  • 以數據爲核心:圍繞「將影響誰」進行編寫。這就是面向對象編程(OOP),圍繞數據及爲數據嚴格定義的接口來組織程序,用數據控制對代碼的訪問。

類和對象是面向對象中的兩個重要概念。算法

  • 類:是對事物的抽象,如人類、球類等。類有靜態屬性和靜態方法,類沒法訪問動態屬性和動態方法;
  • 對象:是類的一個實例,如足球、籃球。對象有動態屬性和動態方法,對象能夠訪問靜態屬性和靜態方法;

面向對象的特性:shell

  1. 惟一性:對象是惟一的,不存在兩個相同的對象,除非他們是同一個對象。就像我做爲一個對象,世界上只有一個我;
  2. 分類性:對象是可分類的,好比動物、食物。

實例說明:球類能夠對球的特徵和行爲進行抽象,而後能夠實例化一個真實的球實體出來。好比咱們對人類進行實例化,能夠實例化出張3、李4、王五等。數據庫

全部編程語言的最終目的都是提供一種抽象方法。在機器模型("解空間"或「方案空間」)與實際解決的問題模型(「問題空間」)之間,程序員必須創建一種聯繫。面向對象是將問題空間中的元素以及它們在解空間中的表示物抽象爲對象,並容許經過問題來描述問題而不是方案。能夠把實例想象成一種新型變量,它保存着數據,但能夠對自身的數據執行操做。編程

類型由狀態集合(數據)和轉換這些狀態的操做集合組成。數組

類是抽象的概念,實例纔是具體的。可是要先設計類,才能完成實例化。類是定一個多個同一類型對象共享的結構和行爲(數據和代碼)。就像 list 就是一種類型,使用 list. 而後就可tab補全一堆方法,可是咱們使用 list.pop() 是會報錯的,由於它是一個抽象的概念。緩存

類內部包含數據和方法這兩個核心,兩者都是類成員。其中數據被稱爲成員變量或實例變量;而方法被稱爲成員方法,它被用來操縱類中的代碼,用於定義如何使用這些成員變量。所以一個類的行爲和接口是經過方法來定義的。

方法和變量就是數據和代碼,若是是私有的變量和方法,只可以在實例內部使用。若是是公共的,能夠在實例外部調用。

在面對對象的程序設計中,全部的東西都是對象,咱們儘量把全部的東西都設計爲對象。程序自己也就是一堆對象的集合,若是要有對象,事先要有類,若是沒有類,用戶就要自定義類,因此用戶本身寫的類就成爲自定義類型。也就是說若是程序裏面有類,那咱們就能夠直接建立實例,若是沒有那咱們就要建立,而後實例化。程序的運行過程就是這些對象彼此之間互相操做的過程,經過消息傳遞,各對象知道本身該作什麼。若是傳遞消息?每一個對象都有調用接口,也就是方法,咱們向方法傳遞一個參數就表示咱們調用了該對象的方法,經過參數傳遞消息。從這個角度來說,消息就是調用請求。

每一個對象都有本身的存儲空間,並可容納其餘對象。好比咱們定義列表l1,裏面有三個元素,那l1是對象,三個元素也是對象。經過封裝現有對象,咱們能夠製做新型對象。每一個對象都屬於某一個類型,類型即爲類,對象是類的實例。類的一個重要特性爲「能發什麼樣的消息給它」,同一個類的全部對象都能接受相同的消息。類的消息接口就是它提供的方法,咱們使用l1.pop(),就至關於給類發送了消息。而不一樣的類消息的接口並不相同,就像咱們不能對字串類型使用pop方法同樣。

定義一個類後,能夠根據須要實例化出多個對象,如何利用對象完成真正有用的工做?必須有一種辦法能向對象發出請求,令其作一些事情。這就是所謂的方法,這些方法加起來就表現爲該類的接口。所以每一個對象僅能接受特定的請求,對象的「類型」或「類」規定了它的接口類型。

數據保存在變量中,變量就是所謂的屬性,方法就是函數。

類間的關係:

  • 依賴("uses-a"):一個類的方法操縱另外一個類的對象;
  • 聚合("has-a"):類 A 的對象包含類 B 的對象;
  • 繼承("is-a"):描述特殊與通常關係。

面對對象的特徵:

  • 封裝(Encapsulation):隱藏實現方案細節,並將代碼及其處理的數據綁定在一塊兒的一種編程機制,用於保證程序和數據不受外部干擾且不會被誤用。類把須要的變量和函數組合在一塊兒,這種包含稱爲封裝。好比 list.pop() 實現的細節咱們並不知道,這就是一種封裝;
  • 繼承(Inheritance):一個對象得到另外一個對象屬性的過程,用於實現按層分類的概念。一個深度繼承的子類繼承了類層次中它的每一個祖先的全部屬性,所以便有了超類、基類、父類(都是上級類)以及子類、派生類(繼承而來);
  • 多態(Polymorphism):容許一個接口被多個通用的類動做使用的特性,具體使用哪一個動做於應用場合相關。一個接口多種方法。意思是,一樣是 x+y,若是 xy 都是數字,那就是從數學運算;若是 xy 是字串,那就是字串鏈接;若是是列表,則是列表鏈接,這就是一個接口多種方法。用於爲一組相關的動做設計一個通用的接口,以下降程序複雜性。

在幾乎全部支持面向對象的語言中,都有 class 關鍵字,而且這個關鍵字和麪向對象息息相關。而在 Python 中,經過 class 關鍵字來定義一個類。

咱們定義了一個類以後,只要在程序中執行了class class_name,就會在內存中生成以這個類名被引用的對象。可是類中的代碼並不會真正執行,只有在實例化時纔會被執行。裏面的方法也不會執行,只有對實例執行方法時纔會執行。類是對象,類實例化出來的實例也是對象,叫實例對象。所以類包含了類對象和實例對象,類對象是能夠調用的對象,而實例對象只能調用實例中的方法。

>>> type(list)
<type 'type'>
>>> l1 = [1, 2, 3]
>>> type(l1)
<type 'list'>
複製代碼

list 是類,l1 是類實例化後的對象。

實例化

建立對象的過程稱之爲實例化。當一個對象被建立後,包含三個方面的特性:對象句柄、屬性和方法。句柄用於區分不一樣的對象,對象的屬性和方法與類中的成員變量和成員函數對應。

定義一個最簡單的類:

>>> class TestClass():
...   pass
... 
>>> type(TestClass)
<type 'classobj'> # 類對象
複製代碼

調用這個類,讓其實例化一個對象:

>>> obj1 = TestClass() # 這就是實例化的過程
>>> type(obj1)
<type 'instance'> # 這是一個實例
複製代碼

經過 obj1 = TestClass() 實例化了一個對象,之因此在類名後加上括號表示執行這個類中的構造器,也就是類中的 __init__ 方法。其實就和函數名後面加上括號表示執行這個函數是同樣的道理。

從上面能夠看出實例初始化是經過調用類來建立實例,語法爲:

instance = ClassName(args…)
複製代碼

Python 中,class 語句相似 def,是可執行代碼,直到運行 class 語句後類纔會存在:

>>> class FirstClass: # 類名
        spam = 30 # 類數據屬性
        def display(self): # 類方法,屬於可調用的屬性
            print self.spam

>>> x = FirstClass() # 建立類實例,實例化
>>> x.display() # 方法調用
複製代碼

class 語句中,任何賦值語句都會建立類屬性,每一個實例對象都會繼承類的屬性並得到本身的名稱空間。

>>> ins1 = FirstClass()
>>> ins1.
ins1.__class__   ins1.__doc__     ins1.__module__  ins1.display(    ins1.spam

# 這個類就出現了全部的方法,能夠看到spam是屬性
複製代碼

封裝

封裝是面對對象的三大特性之一。在瞭解封裝以前,咱們必須知道什麼是 self。

self是啥

經過下面的例子就知道 self 是啥了。

class Foo(object):
    def fun1(self, arg1):
        print(arg1, self)

i1 = Foo()
print(i1)
i1.fun1('hehe')
複製代碼

執行結果:

<__main__.Foo object at 0x00000000006C32B0>
hehe <__main__.Foo object at 0x00000000006C32B0>
複製代碼

能夠看出 i1 和 self 是同一個東西,由此 self 就是實例化對象後的對象自身,也就是 i1。類只有一個,可是實例化的對象能夠有無數個,不一樣的對象的 self 天然都不相同。

self 是一個形式參數,python 內部自動傳遞。

在瞭解了什麼是 self 以後,如今就能夠聊聊封裝了。看下面的例子:

class Foo(object):
    def fetch(self, start):
        print(start)

    def add(self, start):
        print(start)

    def delete(self, start):
        print(start)
複製代碼

上面的代碼中,一樣的參數 start 被傳遞到了三個函數中,這樣就顯得很累贅,可否不須要這麼麻煩呢?確定是能夠的。以下:

class Foo(object):
    def fetch(self):
        print(self.start)

    def add(self):
        print(self.start)

    def delete(self):
        print(self.start)

obj1 = Foo()
obj1.start = 'hehe'
obj1.fetch()
複製代碼

修改後三個函數再也不接受參數,這就達到了咱們的需求。因爲 self 就是對象自己,所以 self.start 就是咱們傳遞的「hehe」,這就是類的封裝。

經過在對象中封裝數據,而後在類中經過 self 進行獲取。這是函數式編程沒法作到的。這只是類封裝的一種方式,也是一種非主流的方式,下面將會提到主流的方式。

構造器

構造器就是所謂 __init__,它是類的內置方法。建立實例時,Python 會自動調用類中的 __init__ 方法。

class Foo(object):
    def __init__(self):
        print('init')

Foo()
複製代碼

執行結果:

init
複製代碼

能夠看到,咱們只要在類名的後面加上括號,就會自動執行類中的 __init__ 函數。經過 __init__ 的這種特性,咱們就能夠實現主流的封裝方式。

咱們能夠看到 __init__ 中並無 return 語句,可是類初始化後的返回值卻並不爲空,所以,實例化一個對象時,還會執行其餘的方法。咱們能夠得出結論:__init__ 不是建立對象,它作的只是初始化對象。

實例化一個對象的過程爲:

  1. 建立對象;
  2. 對象做爲 self 參數傳遞給 __init__
  3. 返回 self。

以上就是一個對象建立的過程,事實上這個過程咱們是能夠手動控制的。

class Foo(object):
    def __init__(self):
        self.start = 'hehe'

    def fetch(self):
        print(self.start)

    def add(self):
        print(self.start)

    def delete(self):
        print(self.start)

obj1 = Foo()
obj1.fetch()
複製代碼

這種方式就比較主流了,當咱們要封裝多個變量時,能夠經過向 __init__ 函數中傳遞多個參數實現。

class Foo(object):
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def fun1(self):
        print('姓名:{},年齡:{}'.format(self.name, self.age))


obj1 = Foo('小紅', 7)
obj2 = Foo('小明', 23)
obj1.fun1()
obj2.fun1()
複製代碼

執行結果:

姓名:小紅,年齡:7
姓名:小明,年齡:23
複製代碼

__init__ 方法被稱爲構造器,若是類中沒有定義 __init__ 方法,實例建立之初僅是一個簡單的名稱空間。類的 __varname__ 這種方法會被 python 解釋器在某些場景下自動調用,就向a+b實際上調用的是 a.__add__(b);l1 = ['abc', 'xyz'] 其實是調用 list.__init__()

構造函數的做用就是不須要咱們手動調用類中的屬性或方法,若是想要在實例化成對象的時候執行,就能夠將操做寫入到 __init__ 方法下。

析構器

析構器又稱爲解構器,定義的是一個實例銷燬時的操做。也就是當使用 del() 函數刪除這麼一個類時,它會自動調用這個類中的 __del__。可是通常而言,解釋器會自動銷燬變量的,所以大多狀況下,析構函數都無需重載,可是構造器則不一樣,它是實現實例變量的一種重要接口。

析構函數就是用於釋放對象佔用的資源,python 提供的析構函數就是 __del__()__del__() 也是可選的,若是不提供,python 會在後臺提供默認析構函數。

析構器會在腳本退出以前執行,咱們能夠用它來關閉文件:

class People(object):
    color = 'yellow'
    __age = 30

    def __init__(self,x):
        print "Init..."
        self.fd = open('/etc/passwd')

    def __del__(self):
        print 'Del...'
        self.fd.close()

ren = People('white')
print 'Main end' # 經過這個判斷__del__是否在腳本語句執行完畢後執行
複製代碼

能夠看出是在腳本退出以前執行的:

[root@node1 python]# python c3.py 
Init...
Main end
Del...
複製代碼

下面是一個析構器的示例:

class Animal:
  name = 'Someone' # 數據屬性(成員變量)
  def __init__(self,voice='hi'): # 重載構造函數
    self.voice = voice # voice有默認值
  def __del__(self): # 這個del就是析構函數,可是它沒有起到任何做用,由於pass了
    pass
  def saysomething(self): # 方法屬性(成員函數)
    print self.voice

>>> tom = Animal()
>>> tom.saysomething()
hi # 默認值爲hi
>>> jerry = Animal('Hello!')
>>> jerry.saysomething()
Hello!
複製代碼

例二:

>>> class Person:
...     def __init__(self,name,age): # 定義一個構造器
...         print 'hehe'
...         self.Name = name
...         self.Age = age
...     def test(self):
...         print self.Name,self.Age
...     def __del__(self): # 定義解構器
...         print 'delete'
...
>>> p = Person('Tom',23)
hehe
>>> del(p) # 刪除實例時當即調用解構器
delete
複製代碼

做用域

函數是做用域的最小單元,那麼在類中有何表現呢?

class E:
    NAME = 'E' # 類的直接下級做用域,叫作類變量

    def __init__(self, name):
        self.name = name # 關聯到對象的變量,叫作實例變量

>>> e = E('e')
>>> e.NAME
Out[4]: 'E'
>>> E.NAME
Out[5]: 'E'
複製代碼

從上面能夠看出,類變量對類和實例均可見。

>>> E.name
Traceback (most recent call last):
  File "/usr/local/python3/lib/python3.6/site-packages/IPython/core/interactiveshell.py", line 2862, in run_code
    exec(code_obj, self.user_global_ns, self.user_ns)
  File "<ipython-input-6-b6daf181be33>", line 1, in <module>
    E.name
AttributeError: type object 'E' has no attribute 'name'
複製代碼

能夠看到,實例變量對實例化後的對象可見,但對類自己並不可見。

>>> e2 = E('e2')
>>> e2.NAME
Out[8]: 'E'
複製代碼

能夠看到,全部實例共享類變量。可是,當其中一個實例修改了類變量呢?

>>> e2.NAME = 'e2'
>>> e.NAME
Out[10]: 'E'
複製代碼

既然共享了,爲何其中一個實例修改後不會影響到其餘實例呢?實例變量究竟是不是共享的呢?咱們再看一個例子。

>>> e.xxx = 1 # 能夠給對象任意增長屬性
>>> e2.xxx
Traceback (most recent call last):
  File "/usr/local/python3/lib/python3.6/site-packages/IPython/core/interactiveshell.py", line 2862, in run_code
    exec(code_obj, self.user_global_ns, self.user_ns)
  File "<ipython-input-13-8ca2718f6555>", line 1, in <module>
    e2.xxx
AttributeError: 'E' object has no attribute 'xxx'
複製代碼

之因此出現這樣的狀況,是由於 Python 可動態的給對象增減屬性。當給實例的類變量增長屬性時,至關於動態的給這個實例增長了一個屬性,覆蓋了類變量。由於,類變量是共享的這句話並無錯。

咱們繼續往下看:

>>> E.NAME = 'hehe' # 直接修改類變量
>>> e.NAME
Out[15]: 'hehe'
>>> e2.NAME
Out[16]: 'e2'
複製代碼

不要感到慌張和迷茫,這裏剛好說明以前的說法都是正確的。由於 e.NAME 並無修改,由於使用的仍然是類變量,當類變量修改了,經過 e 去訪問時確定也會發生變化。而 e2 因爲以前修改了,由於這個類變量被覆蓋了,變成了這個對象的私有屬性了,所以不受類變量的影響。

所以,始終都要牢記 Python 中的一大準則,賦值即建立

屬性的查找順序

事實上經過 e.NAME 訪問,至關於 e.__class__.NAME。而 e.NAME = 1 至關於 e.__dict__['NAME'] = 1。雖然如此,可是會發生下面這樣的狀況。

>>> e.NAME
Out[6]: 'E'
>>> e.__dict__['NAME'] = 1
>>> e.NAME
Out[8]: 1
>>> e.__class__.NAME
Out[9]: 'E'
複製代碼

經過 e.NAME 和 e.__class__.NAME 訪問的結果卻不同,這是爲何呢?這就涉及到屬性的查找順序了:

__dict__ -> __class__
複製代碼

因爲在 __dict__ 中能夠找到 NAME,因此就直接返回了,而不會在 __class__ 中繼續找。

__class__ 是實例對類的一個引用。所以 e.__class__.NAME 這個值就是類中 NAME 的值,它不會改變。而咱們修改實例的屬性,則是添加到實例的 __dict__ 中。

裝飾器裝飾類

給類加裝飾器就是動態的給類增長一些屬性和方法。

看下面的例子:

def set_name(cls, name):
    cls.NAME = name
    return cls

class F:
    pass

>>> F1 = set_name(F, 'F')
>>> F1.NAME
Out[16]: 'F'
複製代碼

事實上 set_name 就至關於一個裝飾器。那咱們可使用裝飾器的語法將其重寫一下:

def set_name(name):
    def wrap(cls):
        cls.NAME = name
        return cls
    return wrap
    
@set_name('G')
class G:
    pass
 
>>> G.NAME
Out[19]: 'G'
複製代碼

結果證實是沒有問題的,其實使用裝飾器語法就至關於:

G = set_name('G')(F) # F 是前面定義的類
複製代碼

還能夠經過裝飾器給類添加方法:

def print_name(cls):
    def get_name(sel): # 必須傳遞一個參數給它,否則不能經過實例來調用
        return cls.__name__
    cls.__get_name__ = get_name
    return cls

@print_name
class H:
    pass

>>> h = H()
>>> h.__get_name__()
Out[24]: 'H'
複製代碼

只不過類裝飾器一般用於給類增長屬性的,而增長方法則有更好的方式。

屬性和方法

類中的變量稱爲屬性、函數稱爲方法。它們又有靜態屬性、靜態方法、動態屬性、動態方法、類方法等之分。

方法的定義都是類級的,可是有的方法使用實例調用,用的方法經過類調用。

實例方法和屬性

實例方法和屬性都是與 self 相關的,所以只能經過實例進行訪問。實例方法的第一個參數是實例名,默認便是如此。因爲類根本不知道實例(self)是什麼(由於尚未實例化),所以不能經過類直接實例方法和實例屬性。

class Foo:
    def __init__(self, name):
        self.name = name # 實例屬性

    def f1(self): # 實例方法
        print('f1')
複製代碼

類方法和屬性

類屬性前面提到過了,定義在類做用域下的變量就是類屬性。它能夠經過類和實例直接訪問。

類方法相似於靜態方法,它能夠經過類直接訪問。與靜態方法的區別在於,它能夠獲取當前類名。第一個參數爲類自己的方法叫作類方法。類方法能夠經過實例進行調用,可是第一個參數依然是類自己。

class Foo:
 @classmethod # 修飾爲類方法
    def f2(cls): # 必須接受一個參數
複製代碼

類方法必須接受一個參數,它是由類自動傳遞的,它的值爲當前類名。也就是說,經過 classmethod 裝飾器會將自動傳遞給方法的第一個參數(以前爲實例名)改成類名。而被裝飾的方法的參數名和 self 同樣,不強制要求爲 cls,只是習慣這麼寫而已。

類方法的最大的用處就是無需實例化便可使用。

靜態方法

不一樣於實例方法和類方法的必須擁有一個參數,靜態方法不須要任何參數。

class Foo:
 @staticmethod # 裝飾爲靜態方法
    def f1(): # 沒有任何參數
        print('static method')
複製代碼

被 staticmethod 裝飾器裝飾後,訪問的時候不會自動傳遞第一個參數。靜態方法和類方法同樣,能夠同時被類和實例訪問。

class Foo:
 @staticmethod
    def f1():
        print('static method')

    def f2(): # 能夠被類訪問
        print('hehe')
複製代碼

f1 和 f2 的區別在於,f2 沒法經過實例訪問。

私有方法和屬性

以雙下劃線開頭,且非雙下劃線結尾的函數/變量就是私有方法/屬性,在類的外部沒法訪問。咱們能夠得出結論:全部以雙下劃線開頭,且非雙下劃線結尾的成員都是私有成員。

經過下面的例子能夠看到它的用處。

class Door:
    def __init__(self, number, status):
        self.number = number
        self.__status = status

    def open(self):
        self.__status = 'opening'
    
    def close(self):
        self.__status = 'closed'

>>> door = Door(1, 'closed')
>>> door.__status # 直接訪問會報錯
Traceback (most recent call last):
  File "/usr/local/python3/lib/python3.6/site-packages/IPython/core/interactiveshell.py", line 2862, in run_code
    exec(code_obj, self.user_global_ns, self.user_ns)
  File "<ipython-input-35-d55234f04e7f>", line 1, in <module>
    door.__status
AttributeError: 'Door' object has no attribute '__status'
複製代碼

可是能夠直接修改它的屬性:

>>> door.__status
Out[37]: 'hehe'
>>> door.open()
>>> door.__status
Out[39]: 'hehe'
複製代碼

雖然賦值即定義,可是仍是有些沒法接受。在外面應該不能修改它纔對。

私有屬性雖然沒法直接訪問,可是並不絕對,Python 提供了訪問它的方法。

_類名 + 私有屬性
複製代碼

好比這麼訪問:

door._Door__status
複製代碼

所以,嚴格的說,Python 中沒有真正的私有成員。

咱們能夠經過這個方式修改私有屬性:

door._Door__status = 'hehe'
複製代碼

但除非真的有必要,而且清楚的知道會有什麼後果,不然不要這麼幹。

類中還有以單下劃線開頭的屬性,這是一種慣用法,標記它爲私有屬性,可是解釋器並非將其當作私有屬性處理。

property

property 裝飾器會把一個僅有 self 參數的函數變成一個屬性,屬性的值爲方法的返回值。

class Foo:
 @property
    def f1(self):
        print('f1')


obj = Foo()
obj.f1 # 不須要加括號了
複製代碼

結合以前的裝飾器,理解下面的例子:

def f(fn):
 @property
    def abc(self):
        print('abc')
        fn(self)
    return abc

class A:
 @f
    def t1(self):
        print('t1')

a = A()
a.t1
複製代碼

經過 property 能夠將方法修飾爲字段,可是屬性的值能夠修改,而使用 property 修飾的函數的返回值卻沒法修改,由於它沒法接受參數。從這裏看,property 好像就只能在調用的時候少些兩個括號而已,可是它並無這麼簡單。

class Foo:
    def __init__(self, name):
        self.name = name

    @property
    def f1(self):
        return ('f1')

>>> obj = Foo('hello')
>>> obj.name
hello
>>> obj.name = 'hehe' # 能夠修改
>>> obj.name
Out[6]: 'hehe'
>>> obj.f1 = 'xxx' # 這麼確定報錯
Traceback (most recent call last):
  File "/usr/local/python3/lib/python3.6/site-packages/IPython/core/interactiveshell.py", line 2862, in run_code
    exec(code_obj, self.user_global_ns, self.user_ns)
  File "<ipython-input-7-6e871c456103>", line 1, in <module>
    obj.f1 = 'xxx'
AttributeError: can't set attribute
複製代碼

因爲 property 裝飾器限制了函數不能接收參數,所以不能給它傳參,也就難以修改裏面的值了。可是,若是想要修改 property 函數中的值也是能夠的,這就用到了它的第二個功能了。

class Foo:
    def __init__(self, name):
        self.name = name

 @property
    def f1(self):
        return self.name

 @f1.setter # f1 是函數名,必須和 property 修飾的函數一致
    def f1(self, value):
        self.name = value

obj = Foo('hello')
print(obj.f1) # 結果是 hello
obj.f1 = 'xxx'
print(obj.f1) # 結果是 xxx
複製代碼

所以,property setter 裝飾器能夠把一個方法轉化爲對此賦值,但此方法有必定要求:

  1. 同名;
  2. 必須接收 self 和 value 兩個參數,value 爲所賦的值。

有了 property setter 裝飾器以後,被 property 裝飾的函數就能夠接收參數了。相應的,咱們能夠經過這個參數來達到咱們的一些目的。

除了 setter 以外,還有一個 deleter 的裝飾器,這也是 property 的第三個功能。當刪除 property 裝飾器裝飾的函數(因爲被 property 裝飾,所以函數變成屬性)時,會調用 deleter 裝飾的函數。

class Foo:
    def __init__(self, name):
        self.name = name
    
 @property
    def f1(self):
        return self.name
    
 @f1.deleter
    def f1(self):
        print('hehe')

>>> obj = Foo('f1')
>>> del obj.f1
hehe
複製代碼

事實上 del 是不能刪除方法的,可是因爲函數被 property 裝飾後會變成屬性,所以能夠被刪除。

能夠看出 property 很強大,它不只能夠做爲裝飾器,還能夠引伸出來兩個裝飾器,只不過須要定義三個函數。其實它徹底能夠定義的更簡單,而達到相同的效果。下面就是將以前定義的三個函數經過一行代碼取代。

f1 = property(lambda self: self.name, lambda self, value: self.name = value, lambda: self: print('hehe'))
複製代碼

f1 這個函數接收三個函數做爲參數,第一個函數是必須的,後面兩個能夠省略。正好對應 property, f1.setter, f1.deleter 裝飾的三個函數。

下面定義了兩個類,第二個類比第一個類中多了一個繼承對象 object,其餘的都同樣,而且調用方式也相同。咱們看看結果,而後進行對比:

class test1:
    def __init__(self,flag):
        self.__pravite = flag
    
 @property
    def show(self):
        return self.__pravite

class test2(object):
    def __init__(self,flag):
        self.__pravite = flag
    
 @property
    def show(self):
        return self.__pravite

t1 = test1('t1')
print t1.show
t1.show = 'x1'
print t1.show

t2 = test2('t2')
print t2.show
t2.show = 'x2'
print t2.show
複製代碼

一樣進行修改,而後進行訪問。如下是執行結果:

t1
x1
t2
Traceback (most recent call last):
  File "E:\workspace\test\main\t1.py", line 48, in <module>
    t2.show = 'x2'
AttributeError: can't set attribute
複製代碼

能夠看出,若是不繼承 object,不使用 @xxx.setter 裝飾器,私有屬性是能夠直接修改的。可是若是繼承了 object,那就必須使用不使用 @xxx.setter 裝飾器了,否則沒法修改。

繼承

繼承是面對對象的重要特性之一,前面提到的都屬於「封裝」。繼承是相對兩個類而言的父子關係,子類繼承了父類的全部公有屬性和方法,繼承實現了代碼的重用。Python 容許多繼承,也就是說一個類能夠繼承多個父類,這是其餘面向對象編程語言(C#, Java 等)所不具有的。多繼承時,哪一個類放在前面,哪一個類就最終繼承。也就是說兩個類中都有相同的屬性或方法時,寫在前面的類是子類繼承的類。

子類也稱爲派生類,父類也稱爲基類。

派生類能夠繼承父類中的全部內容,當派生類和父類中同時存在相同的內容時,優先使用本身的。也就是說當實例化子類時,若是執行父類中 self.abc 代碼,首先會在當前類(也就是子類中)查找 abc,由於實例化的是子類而非父類,只要找不到纔會去父類中找。若是是多繼承,也是一級一級的往上找。

若是子類中沒有定義初始化方法,實例化時會執行父類中的初始化方法。而若是子類中存在,就不會執行父類的。若是想要執行父類中的初始化方法,可使用 super 函數,下面會講到。

繼承描述了基類的屬性如何「遺傳」給派生類。

  • 子類能夠繼承它的基類的任何屬性,包括數據和方法;
  • 一個未指定基類的類,其默認有一個名爲 object 的基類;
  • Python 容許多重繼承,也就是說能夠有多個並行的父類。

建立子類時,只須要在類名後跟一個或從其中派生的父類:

class SubClassName(ParentClass1[,ParentClass2,…])
複製代碼

凡是公有的都能繼承,凡是私有的都不能繼承。由於私有的繼承後會更名,這就會致使找不到(改下名後就能訪問到了)。原來是什麼,繼承過來仍是什麼。類變量繼承過來仍是類變量。

當咱們對一個子類進行實例化時,這個實例化對象也是父類的實例化對象,好比:

class A:
    pass
class B(A):
    pass

>>> a = B()
>>> isinstance(a, A)
Out[6]: True
複製代碼

方法重寫

當子類和父類中都擁有相同的成員(包括屬性和方法)時,子類會使用本身的而不是父類的,這是繼承的規則。可是這樣會致使子類沒法使用父類中的同名方法。爲了解決這一問題,Python 中引入了 super 類。

也就是說只有要進行方法重寫的時候纔會使用 super。

super 的使用方法:

super() -> same as super(__class__, <first argument>)
super(type) -> unbound super object
super(type, obj) -> bound super object; requires isinstance(obj, type)
super(type, type2) -> bound super object; requires issubclass(type2, type)
複製代碼

能夠看出 super 能夠不接受參數,也能夠最多接收兩個參數。當 super 不帶參數時,它的意義和 super(子類名, self) 相同。

# 定義一個父類
class Base:
    def print(self):
        print('Base.print')

# 子類中也有 print 方法,很顯然會覆蓋父類的 print 方法
class Sub(Base):
    def print(self):
        print('Sub.print')
    def foo(self):
        # 經過 super 調用父類的同名方法,如下兩種寫法做用相同
        super(Sub, self).print()
        super().print()
        
>>> Sub().foo() # 明顯兩種寫法做用相同
Base.print
Base.print
複製代碼

這是針對實例方法的,它一樣能夠針對類方法:

class Base:
 @classmethod
    def cls_print(cls):
        print('Base.cls_print')

class Sub(Base):
 @classmethod
    def cls_print(cls):
        print('Sub.cls_print')

 @classmethod
    def cls_foo(cls):
        # 因爲是類方法,所以能夠直接經過父類進行訪問
        Base.cls_print()
        # 能夠針對類方法
        super().cls_print()
        # 這種會報錯
        #super(Base, cls).cls_print()
        
>>> Sub().cls_foo()
Base.cls_print
Base.cls_print
複製代碼

咱們能夠得出結論,90% 的狀況下 super 不須要帶參數。下面介紹的就是那 10%。

class Base:
    def print(self):
        print('Base.print')

class Sub(Base):
    def print(self):
        print('Sub.print')

# 咱們定義了子類,可是繼承的是 Sub,父子孫中都擁有同名的方法
class Subsub(Sub):
    def print(self):
        print('Subsub.print')

    def foo(self):
        # 當要調用父類的父類中的同名方法時,super 就要帶參數了
        super(Sub, self).print()

>>> Subsub().foo()
Base.print
複製代碼

回到一開始列出的 super 的使用方法,能夠得出結論:super 代理 TYPE 的父類方法,而且使用 obj 綁定。第一個參數是指調用誰的直接父類,第二個參數是指調用時,傳遞給方法的第一個參數。

帶參數的__init__

看一個示例:

class Base:
    def __init__(self, a, b):
        # 定義兩個私有屬性
        self.__a = a
        self.__b = b

    def sum(self):
        return self.__a + self.__b

class Sub(Base):
    def __init__(self, a, b, c):
        self.c = c
        self.__a = a
        self.__b = b

>>> Sub(1, 2, 3).sum()
Traceback (most recent call last):
  File "/usr/local/python3/lib/python3.6/site-packages/IPython/core/interactiveshell.py", line 2862, in run_code
    exec(code_obj, self.user_global_ns, self.user_ns)
  File "<ipython-input-3-e982c4a04055>", line 1, in <module>
    Sub(1, 2, 3).sum()
  File "<ipython-input-2-7fde2b10dc1f>", line 7, in sum
    return self.__a + self.__b
AttributeError: 'Sub' object has no attribute '_Base__a'
複製代碼

報錯了。若是初始化函數中的屬性不是私有的話,是不會報錯的。可是私有屬性必定會報錯,由於私有屬性是沒法繼承的。爲了讓它不報錯,就能夠用到 super 了。

class Base:
    def __init__(self, a, b):
        self.__a = a
        self.__b = b

    def sum(self):
        return self.__a + self.__b

class Sub(Base):
    def __init__(self, a, b, c):
        self.c = c
        # 直接調用父類的初始化方法
        super().__init__(a, b)
     
>>> Sub(1, 2, 3).sum()
Out[6]: 3
複製代碼

若是繼承父類,那麼定義在父類 __init__ 中的相同的屬性會覆蓋子類中的。

若是父類含有一個帶參數的初始化方法的時候,子類必定須要一個初始化方法,而且在初始化方法中調用父類的初始化方法。

Python2 中還能經過下面的方法繼承父類中同名的方法,可是很顯然 super 徹底能夠替代它,由於 super 能夠指定繼承哪個父類中同名的成員。

父類.__init__(self[, arg1, arg2...])
複製代碼

super獲取類變量

前面經過 super 獲取的是方法,此次獲取的是變量:

class Base:
    NAME = 'BASE'


class Sub(Base):
    NAME = 'SUB'

    def print(self):
        print(self.NAME)
        print(super(Sub, Sub).NAME)
        
>>> Sub().print()
SUB
BASE
複製代碼

實例變量是沒法獲取的,由於父類並無實例化,實例變量是不存在的,所以確定是沒法繼承的。

還有一種狀況:

class Base:
    NAME = 'BASE'

class Sub(Base):
    NAME = 'SUB'

    def print(self):
        print(self.NAME)
        print(super(Sub, Sub).NAME)
        print(Base.NAME)
複製代碼

最後兩行在單繼承環境下沒有區別,可是在多級繼承時存在區別。

多繼承

python 支持多繼承,而 Python3 中的全部類都會繼承 object 這個類,所以下面這三種寫法意義相同:

class A:
    pass
    
class A(object):
    pass

class A():
    pass
複製代碼

咱們能夠看到 object 中包含的全部方法和屬性:

In [4]: dir(A())
Out[4]: 
['__class__',
 '__delattr__',
 '__dict__',
 '__dir__',
 '__doc__',
 '__eq__',
 '__format__',
 '__ge__',
 '__getattribute__',
 '__gt__',
 '__hash__',
 '__init__',
 '__init_subclass__',
 '__le__',
 '__lt__',
 '__module__',
 '__ne__',
 '__new__',
 '__reduce__',
 '__reduce_ex__',
 '__repr__',
 '__setattr__',
 '__sizeof__',
 '__str__',
 '__subclasshook__',
 '__weakref__']
複製代碼

因此,咱們只要定義一個類,天生就擁有這麼多的屬性和方法。

多繼承的寫法:

class Sub(Base, Base2) # 繼承列表中有多個類就表示多繼承
複製代碼

多繼承會把繼承列表中的全部公有成員都繼承過來,當有同名成員時,就會有一個繼承順序了。

多繼承的查找順序

在討論類繼承順序以前,咱們首先要了解 MRO,它的本意就是方法查找順序。它要知足幾個條件:

  • 本地優先:本身定義或重寫的方法優先。本地沒有的,按照繼承列表,從左往右查找;
  • 單調性:全部子類,也要知足查找順序。也就是說 A 繼承 B C,A 會先找 B 再找 C。可是在 A 查找以前,B 若是有多個繼承,那麼它先得按查找順序查找。

若是定義一個多繼承的類,若是不能知足 MRO 的話,會拋出 MRO 的異常。

class A:
    pass

class E(A):
    pass

class F(A, E):
    pass

>>> F()
Traceback (most recent call last):
  File "/usr/local/python3/lib/python3.6/site-packages/IPython/core/interactiveshell.py", line 2862, in run_code
    exec(code_obj, self.user_global_ns, self.user_ns)
  File "<ipython-input-9-491a467e42f0>", line 7, in <module>
    class F(A, E):
TypeError: Cannot create a consistent method resolution
order (MRO) for bases A, E # 拋出 MRO 異常,緣由下面講
複製代碼

MRO 是能夠看到的,由於類中存在這個屬性。

>>> A.__mro__
Out[10]: (__main__.A, object)
>>> E.__mro__
Out[11]: (__main__.E, __main__.A, object)

# 再定義一個 G
class G(E, A):
    pass

>>> G.__mro__
Out[17]: (__main__.G, __main__.E, __main__.A, object)
複製代碼

能夠看到 G 的查找順序是 G -> E -> A -> object,既能知足 A 的順序,也能知足 E 的順序,因此咱們說 G 的定義知足了它們 MRO 的單調性。可是若是 F 可以定義的話,它的查找順序是 F -> A -> E -> object,很顯然既知足不了 A,也不能知足 E,所以就會拋出 MRO 異常。

Python 和其餘語言經過 c3 算法檢測類是否知足了 MRO 的兩個原則,算法的解釋是:

class B(O) -> [B, O] # 最終查找順序要是這樣 class B(A1, A2, ..., An) -> [B] + merge(mro(A1), mro(A2), ..., mro(An), [A1, A2, ..., An, O]) 複製代碼

它實際上會使用遞歸,結果相似於第一行的列表(正確的 MRO)爲退出條件。

merge 有四個步驟:

  1. 遍歷列表。它會先求出 A1 到 An 全部類的 MRO,它會造成一個列表,merge 的參數也就是這個列表;
  2. 看一個列表的首元素,它存在兩種狀況:
    • 它在其餘列表中也是首元素;
    • 它在其餘列表中不存在;
  3. 若是首元素知足上面兩種狀況中的一種,那麼會將其從列表中移除,合併到 MRO;
  4. 不知足的話,拋出異常。不然不斷循環,直到列表爲空。

大體過程是這樣的(其中 G, E, A 都是一個類,O 表示 object,最後的 [E, A, O] 表示繼承的順序):

mro(G) -> [G] + merge(mro[E], mro[A], [E, A, O])
	-> [G] + merge([E, A, O], [A, O], [E, A, O]) # E 在全部列表的首部,所以拿出來
	-> [G, E] + merge([A, O], [A, O], [A, O]) 
	-> [G, E, A] + merge([O], [O], [O])
	-> [G, E, A, O] # 最終就只剩下這個列表
複製代碼

當定義一個類的時候,解釋器會指定 c3 算法來確認 MRO,若是 c3 算法拋出異常,此類不能定義。

咱們應該儘可能避免多繼承。繼承多了很容易就搞懵了,而且 python 邊解釋便運行,若是不調用類,它也不會知道類的定義有沒有錯誤。當咱們覺得繼承是對的時候,可是某天忽然報錯了,可能都不知道是什麼緣由形成的。

如下就是 MRO 的圖形展現。

image_1b0jrmk4ocdh1j8nlem40gjqp9.png-14.7kB

如上圖所示,A 同時繼承 B 和 C,B 繼承 D,C 繼承 E。當 A 中沒有對應的方法時,會先在 B 找,找不到會找 D 而不會找 C,最後找 E。

image_1b0jruq3t14j77rd13db1hujmjam.png-16kB

和上圖基本相同,就多了個 D 和 E 同時繼承 F。這裏 D 找不到時不會找F而是會找 C,F 是最後一個找的。

也就是說若是沒有共同繼承的基類,會一直往上找。而共同繼承的基類最後一個找。

可是還有下面這種狀況:

image_1b0k16v3k20fgch1v0k49o5q52n.png-14.4kB

class D:
    def xxx(self):
        print('xxx')
        self.f()


class C:
    def f(self):
        print('C')


class B(D):
    def f(self):
        print('B')


class A(B, C):
    pass


obj = A()
obj.xxx()
複製代碼

繼承關係和前面同樣,當執行 XXX 方法時,因爲 B 裏面沒有,因此找到了 D。可是 D 中會執行 self.f(),那麼 f 函數會在哪一個函數中找呢?答案是B。

執行 self.f() 時,self 是 obj,而 obj 又是從 A 中實例化而來。所以執行 self.f() 會如今 A 中找 f 這個函數,若是沒有,確定在 B 中找。所以答案是 B。

Mixin

要給一個類動態的增長方法,有多種方式:

  • 能夠經過繼承的方式,可是若是繼承的類是標準庫中的,因爲沒法修改,因此行不通。
  • 經過類裝飾器,惟一的問題是裝飾器沒法繼承。
class Document:
    def __init__(self, content):
        self.content = content

class Word(Document):
    def __init__(self, content):
        super().__init__('word: {}'.format(content))

def printable(cls):
    def _print(self): # 給類加了這個方法
        print('P: {}'.format(self.content))
    cls.print = _print
    return cls

@printable
class PrintableWord(Word):
    def __init__(self, content):
        super().__init__(content)

>>> PrintableWord('abc').print()
P: word: abc
複製代碼
  • Mixin 的方式。它就是繼承一個類,在這個類中增長方法,就能達到給目標類增長功能的目的。
class Document:
    def __init__(self, content):
        self.content = content


class Word(Document):
    def __init__(self, content):
        super().__init__('word: {}'.format(content))


class PrintableMixin:
    def print(self):
        print('P: {}'.format(self.content))


class PrintableWord(PrintableMixin, Word):
    def __init__(self, content):
        super().__init__(content)


>>> PrintableWord('abc').print()
P: word: abc
複製代碼

使用 Minix 的好處在於,經過定義類的方式添加的動態方法是能夠被其餘類繼承的。

class Document:
    def __init__(self, content):
        self.content = content


class Word(Document):
    def __init__(self, content):
        super().__init__('word: {}'.format(content))


class PrintableMixin:
    def print(self):
        result = 'P: {}'.format(self.content)
        print(result)
        return result


class PrintableWord(PrintableMixin, Word):
    def __init__(self, content):
        super().__init__(content)

# 再次被繼承
class PrintToMonitorMixin(PrintableMixin):
    def print(self):
        print('Monitor: {}'.format(super().print()))


class PrintToMonitorWord(PrintToMonitorMixin, Word):
    pass


>>> PrintToMonitorWord('abc').print()
P: word: abc
Monitor: P: word: abc
複製代碼

Mixin 是經過多繼承實現的組合方式。一般來講,組合優於繼承。socketserver 就用到了 Mixin。

多態

多態也是面對對象的特性之一,意思是多種形態。可是不一樣於 C# 和 java,python 中的多態是原生的,這應該也是弱類型語言的特性,所以 python 中的多態不多有提到。

class Foo:
    def f1(self):
        print('Foo')


class Bar:
    def f1(self):
        print('Bar')


def fun(arg):
    arg.f1()


fun(Foo())
fun(Bar())
複製代碼

執行沒有任何問題,由於python中什麼參數都能接收,你給我什麼我就接收什麼。可是在 Java 中函數 fun 不能這麼寫,只能寫成 fun(Foo arg)fun(Bar arg)。這就限定死了它只能接受一種類建立的對象,所以它們纔有實現多態的需求。

其餘語言的多態是這樣實現的:

class Father:
    pass


class Foo(Father):
    def f1(self):
        print('Foo')


class Bar(Father):
    def f1(self):
        print('Bar')


def fun(Father arg):
    arg.f1()


fun(Foo())
fun(Bar())
複製代碼

給它一個父類便可,這樣父類和子類均可以傳遞。經過相同的父類實現多態。Python 中的方法重寫、運算符重載都是多態的體現。

特有方法

前面咱們就已經看到了,定義一個類時,這個類會從 object 中繼承不少以雙下劃線開頭和雙下劃線結尾的成員,這些成員中有的是屬性,有的是方法。

class A:
     pass
 
>>> dir(A)
Out[4]: 
['__class__',
 '__delattr__',
 '__dict__',
 '__dir__',
 '__doc__',
 '__eq__',
 '__format__',
 '__ge__',
 '__getattribute__',
 '__gt__',
 '__hash__',
 '__init__',
 '__init_subclass__',
 '__le__',
 '__lt__',
 '__module__',
 '__ne__',
 '__new__',
 '__reduce__',
 '__reduce_ex__',
 '__repr__',
 '__setattr__',
 '__sizeof__',
 '__str__',
 '__subclasshook__',
 '__weakref__']
複製代碼

__name__

得到類的名字。

>>> A.__name__
Out[9]: 'A'
複製代碼

注意,實例是沒有這個屬性的。

__module__

獲取模塊名。ipython 並不知道它的模塊名,由於結果爲 main:

>>> A.__module__
Out[11]: '__main__'
複製代碼

__doc__

顯示文檔字符串。

>>> A.__doc__
複製代碼

__class__

python 一切皆對象,類也是對象,全部類都是 type 的對象。

>>> A.__class__
Out[13]: type
複製代碼

而實例的 class 則是它的類:

>>> A().__class__
Out[14]: __main__.A
複製代碼

所以咱們能夠獲取實例的類名:

>>> A().__class__.__name__
Out[15]: 'A'
複製代碼

__dict__

針對實例的,它持有全部實例擁有的屬性。咱們給實例增長屬性就是給這個字典增長 key,這也是實例能夠動態增長屬性的緣由。

__dir__

它會獲得實例的全部成員,類並無這個方法。dir() 底層就是調用它。

>>> A().__dir__()
Out[19]: 
['__module__',
 '__dict__',
 '__weakref__',
 '__doc__',
 '__repr__',
 '__hash__',
 '__str__',
 '__getattribute__',
 '__setattr__',
 '__delattr__',
 '__lt__',
 '__le__',
 '__eq__',
 '__ne__',
 '__gt__',
 '__ge__',
 '__init__',
 '__new__',
 '__reduce_ex__',
 '__reduce__',
 '__subclasshook__',
 '__init_subclass__',
 '__format__',
 '__sizeof__',
 '__dir__',
 '__class__']
複製代碼

__hash__

當咱們傳遞一個對象給內置方法 hash() 時,它可以返回一個整數。

>>> hash('abc')
Out[2]: 1751202505306800636
複製代碼

事實上它是調用類的 __hash__ 方法。

class Point:
    def __hash__(self):
        return 1
    
>>> hash(Point())
Out[3]: 1
複製代碼

可是 __hash__ 的返回值必須是一個整數,不然會報錯。

class Point:
    def __hash__(self):
        return 'a'
    
>>> hash(Point())
Traceback (most recent call last):
  File "/usr/local/python3/lib/python3.6/site-packages/IPython/core/interactiveshell.py", line 2862, in run_code
    exec(code_obj, self.user_global_ns, self.user_ns)
  File "<ipython-input-5-a919dcea3eae>", line 1, in <module>
    hash(Point())
TypeError: __hash__ method should return an integer
複製代碼

__hash__ 有什麼用呢?一個類中有這個方法,而且返回一個整數,那麼它的實例對象就是可哈希對象。而字典和集合中只能添加可哈希對象,也就說它們在添加以前會調用 hash 方法。

class Point:
    # 將 __hash__ 直接幹掉
    __hash__ = None
    
 >>> set([Point()]) # 添加時直接拋出異常
Traceback (most recent call last):
  File "/usr/local/python3/lib/python3.6/site-packages/IPython/core/interactiveshell.py", line 2862, in run_code
    exec(code_obj, self.user_global_ns, self.user_ns)
  File "<ipython-input-7-b7932f1140b9>", line 1, in <module>
    set([Point()])
TypeError: unhashable type: 'Point' 
複製代碼

一個類,若是沒有重寫 __hash__ 方法,這個類的每一個對象,將會具備不一樣的 hash 值。這會形成什麼問題呢?咱們定義一個類:

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y
        
>>> a = Point(2, 3)
>>> b = Point(2, 3)
>>> set([a, b])
Out[11]: {<__main__.Point at 0x7f410bc5e0b8>, <__main__.Point at 0x7f410bc5e358>}
複製代碼

事實上咱們認爲 a 和 b 是徹底相同的,可是因爲它們的哈希值不一樣,set 並不會將其當成一個對象,由於沒法經過 set 進行去重。那麼應該怎麼辦呢?咱們須要對 __hash__ 進行重寫。

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __hash__(self):
        return hash('{}:{}'.format(self.x, self.y))

>>> a = Point(2, 3)
>>> b = Point(2, 3)
>>> hash(a) == hash(b)
Out[15]: True
>>> set([a, b])
Out[16]: {<__main__.Point at 0x7f410b399908>, <__main__.Point at 0x7f410b399d68>}
複製代碼

它們 hash 是相等了,可是仍是不能經過 set 進行去重。這是由於 set 不光要檢查元素 hash 值,還會檢查類自己是否相同。很顯然,a 和 b 並不相同:

>>> a == b
Out[17]: False
複製代碼

若是想讓它們相等,那咱們得重寫 __eq__ 方法:

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __hash__(self):
        return hash('{}:{}'.format(self.x, self.y))

    def __eq__(self, other):
        return self.x == other.x and self.y == other.y

>>> a = Point(2, 3)
>>> b = Point(2, 3)
>>> set([a, b]) # 去重成功
Out[21]: {<__main__.Point at 0x7f410b376588>}
複製代碼

一般 __hash__ 會和 __eq__ 同時使用,由於解釋器同時判斷 hash 和實例是否相等。所以當咱們的實例要放在字典或者集合中時,咱們就要在類中實現它們。

__len__

內置函數 len 就是調用類自己的 __len__ 方法。

>>> class Sized:
...     def __len__(self):
...         return 10
...     
>>> len(Sized())
Out[23]: 10
複製代碼

因爲 object 中並無 __len__,所以須要咱們手動實現。須要注意的是,__len__ 必須返回整數,且必須大於 0。

自定義數據結構的時候會用到它。

__bool__

經過 bool 這個內置方法能夠判斷一個對象的真假,事實上就是調用對象自己的 __bool__ 方法。

class O:
    pass

>>> bool(O()) # 默認爲真
Out[24]: True

class O:
    # 定義它爲假
    def __bool__(self):
        return False
    
>>> bool(O()) # 那就爲假
Out[25]: False
複製代碼

事實上並無這麼簡單,由於列表並無實現 __bool__,可是空列表會返回假,列表有元素就返回真。判斷依據是什麼?空列表的 __len__ 是 0。

class Sized:
    def __init__(self, size):
        self.size = size
    
    def __len__(self):
        return self.size
        
>>> bool(Sized(0))
Out[31]: False
>>> bool(Sized(1))
Out[32]: True
複製代碼

當對象沒有實現 __bool__,而實現了 __len__ 時,__len__ 等於 0 返回假,不然爲真;若是這兩種方法都沒有實現,返回真;而當這兩種方法同時出現時,__bool__ 優先級更高。

__bool__ 必須返回 True 和 False。

__str__

當咱們 print 一個對象時,就會調用這個方法。

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    
    def __str__(self):
        return 'Point<{}, {}>'.format(self.x, self.y)
    
>>> print(Point(1, 3))
Point<1, 3>
複製代碼

'{!s}'.format() 也是調用類的 __str__ 方法。它在調試程序、打日誌的時候頗有用。

__repr__

內置方法 repr'{!r}'.format() 就是調用這個方法。str 一般給人讀,而 repr 則是給機器讀的。所以咱們會重寫 str,可是不多會重寫 repr。好比下面這種就適合給機器讀而不適合給人讀:

<__main__.A at 0x7fb30cd64860>
複製代碼

__repr__ 到底實現了什麼呢?當咱們在交互式模式下實例化一個類時:

>>> class A: pass
... 
>>> a = A()
>>> a
Out[11]: <__main__.A at 0x7f9cc1fd1898>
複製代碼

a 的結果就是調用了 __repr__ 方法,好比:

class A:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __repr__(self):
        return 'A({0.x}, {0.y})'.format(self)

>>> a = A(2, 5)
>>> a
Out[16]: A(2, 5)
複製代碼

上面的 format() 方法的使用看上去頗有趣,格式化代碼 {0.x} 對應的是第一個參數的 x 屬性。 所以,0 實際上指的就是 self 自己。做爲這種實現的一個替代,你也可使用 % 操做符,就像下面這樣:

def __repr__(self):
    return 'A(%r, %r)' % (self.x, self.y)
複製代碼

__repr__() 生成的文本字符串標準作法是須要讓 eval(repr(x)) == x 爲真。 若是實在不能這樣子作,應該建立一個有用的文本表示,並使用 <> 括起來。好比:

>>> f = open('file.dat')
>>> f
<_io.TextIOWrapper name='file.dat' mode='r' encoding='UTF-8'>
複製代碼

若是 __str__() 沒有被定義,那麼就會使用 __repr__() 來代替輸出。

__format__

爲了自定義字符串的格式化,咱們須要在類上面定義 __format__() 方法。例如:

_formats = {
    'ymd' : '{d.year}-{d.month}-{d.day}',
    'mdy' : '{d.month}/{d.day}/{d.year}',
    'dmy' : '{d.day}/{d.month}/{d.year}'
    }

class Date:
    def __init__(self, year, month, day):
        self.year = year
        self.month = month
        self.day = day

    def __format__(self, code):
        if code == '':
            code = 'ymd'
        fmt = _formats[code]
        return fmt.format(d=self)
複製代碼

如今 Date 類的實例能夠支持格式化操做了,如同下面這樣:

>>> d = Date(2012, 12, 21)
>>> format(d)
'2012-12-21'
>>> format(d, 'mdy')
'12/21/2012'
>>> 'The date is {:ymd}'.format(d)
'The date is 2012-12-21'
>>> 'The date is {:mdy}'.format(d)
'The date is 12/21/2012'
複製代碼

__format__() 方法給 Python 的字符串格式化功能提供了一個鉤子,這裏須要着重強調的是格式化代碼的解析工做徹底由類本身決定。所以,格式化代碼能夠是任何值。例如,參考下面來自 datetime 模塊中的代碼:

>>> from datetime import date
>>> d = date(2012, 12, 21)
>>> format(d)
'2012-12-21'
>>> format(d, '%A, %B %d, %Y')
'Friday, December 21, 2012'
>>> 'The end is {:%d %b %Y}. Goodbye'.format(d)
'The end is 21 Dec 2012. Goodbye'
>>>
複製代碼

__call__

一個對象中只要存在該方法,那麼就能夠在它的後面加上小括號執行。函數在 Python 也是對象,全部函數都是 function 的實例:

>>> def fn():
...     pass
... 
>>> fn.__class__
Out[4]: function
複製代碼

函數之因此能夠被調用執行,就是由於其內部存在 __call__ 方法,所以咱們只要在類的內部定義這個方法,那麼該類的實例就能夠被調用。而這種對象咱們稱之爲可調用對象

>>> class Fn:
...     def __call__(self):
...         print('called')
...         
>>> Fn()()
called
複製代碼

內置方法 callable 能夠用來判斷一個對象是否可被調用。

它能夠用類來寫裝飾器,讓你和使用函數寫的裝飾器同樣用。之因此使用類來寫裝飾器是由於很是複雜的裝飾器,使用類來寫的話,能夠方便拆分邏輯。而且以前咱們要爲函數保存一些變量須要經過閉包來實現,如今徹底可使用類的 __call__ 方法。用 __call__ 實現可調用對象,和閉包是異曲同工的,一般都是爲了封裝一些內部狀態。

__enter__

詳見上下文管理。

__exit__

詳見上下文管理。

__getattr__

直接看例子:

>>> class A:
...     def __init__(self):
...         self.x = 3
... 
...     def __getattr__(self, item):
...         return 'missing property {}'.format(item)
...     
>>> a = A()
>>> a.x
Out[20]: 3
>>> a.y
Out[21]: 'missing property y'
>>> a.z
Out[22]: 'missing property z'
複製代碼

當一個類定義了 __getattr__ 方法時,若是訪問不存在的成員,會調用該方法。所以一個對象的屬性查找順序爲:__dict__ -> class -> __getattr__。當 dict 和 class 中都不存在時,就會執行 getattr。好比字典的 setdefault 方法。

__setattr__

當一個類實現了 __setattr__ 時,任何地方對這個類增長屬性,或者對現有屬性賦值時,都會調用該方法。

>>> class A:
...     def __init__(self):
...         self.x = 3
... 
...     def __setattr__(self, name, value):
...         print('set {} to {}'.format(name, value))
...         
>>> a = A()
set x to 3
>>> a.y = 5
set y to 5
複製代碼

實例化的時候,因爲實例化方法中存在賦值的行爲,所以觸發 __setattr__。此時的 self.x 沒有賦值,可是咱們是能夠進行賦值的。

>>> class A:
...     def __init__(self):
...         self.x = 3
... 
...     def __setattr__(self, name, value):
...         print('set {} to {}'.format(name, value))
...         self.__dict__[name] = value # 增長這一行便可
...         
>>> a = A()
set x to 3
>>> a.y = 5
set y to 5
>>> a.__dict__
Out[9]: {'x': 3, 'y': 5}
複製代碼

所以 __setattr__ 用在須要對實例屬性進行修改的同時,作一些額外的操做時。

可是事實上這個方法並非那麼好用,它有不少坑,好比:

class A:
    def __init__(self):
        self.x = 3

    def __setattr__(self, name, value):
        print('set {} to {}'.format(name, value))
        setattr(self, name, value) # 換成這個
複製代碼

這個類只要實例化就會將解釋器幹掉。由於 setattr 至關於執行了 self.name = value,可是這一賦值操做就又會觸發 __setattr__,這就造成遞歸了。因爲沒有退出條件,遞歸達到極限後,解釋器退出。

謹慎使用吧。

__delattr__

當刪除實例的屬性時,會調用該方法。

class A:
    def __init__(self):
        self.x = 3
    def __delattr__(self, name):
        print('u cannot delete property'.format(name))
        
a = A()
del a.x
u cannot delete property
複製代碼

它用在保護實例屬性不被刪除。

__getattribute__

只要類中定義了它,那麼訪問實例化對象中的任何屬性和方法都會調用該方法,殺傷力巨大。

>>> class A:
...     NAME = 'A'
... 
...     def __init__(self):
...         self.x = 3
... 
...     def __getattribute__(self, item):
...         return 'hehe'
... 
...     def method(self):
...         print('method')
...         
>>> a = A()
>>> a.x
Out[18]: 'hehe'
>>> a.method
Out[19]: 'hehe'
>>> a.NAME
Out[20]: 'hehe'
複製代碼

所以實例化對象成員查找順序爲:

__getattribute__ -> __dict__ -> __class__.__dict__ -> __getattr__
複製代碼

這玩意基本不會用到。

__get__

詳見描述器。

__set__

詳見描述器。

__getitem__

這個方法是用來在對象後面使用中括號的。咱們之因此可以經過在字典後面加中括號獲取字典裏面 key 對應的值,就是由於 dict 這個類中使用了 __getitem__ 方法。

class Foo:
    def __getitem__(self, item): # 接收中括號中的參數
        print(item)


obj = Foo()
obj['hehe']
複製代碼

執行後輸出hehe。中括號中如今能夠輸入內容了,可是若是使用序列的切片操做呢?python2 中會調用 __getslice__,可是 python3 中仍然調用 __getitem__

>>> class Foo:
    def __getitem__(self, item):
        print(item, type(item))

obj = Foo()
obj[1:4:2]
slice(1, 4, 2) <class 'slice'>
複製代碼

當咱們往中括號中傳遞切片的語法時,它會先調用slice這個類,而後將這個類傳遞給 __getitem__

可是卻沒法跟字典同樣進行賦值,若是想賦值,可使用下面的方法。

__setitem__

class Foo:
    def __setitem__(self, key, value):
        print(key, value)


obj = Foo()
obj['name'] = 'lisi'
複製代碼

這就能夠賦值了。當咱們使用切片賦值時,python2 中會調用 __setslice__ 方法,可是 python3 中仍是調用 __setitem__

>>> class Foo:
    def __setitem__(self, key, value):
        print(key, value, type(key), type(value))

obj = Foo()
obj[1:4:2] = [11, 22, 33]
slice(1, 4, 2) [11, 22, 33] <class 'slice'> <class 'list'>
複製代碼

可是不能使用 del obj['xxx'] 進行刪除。若是想刪除,使用下面的方法。

__delitem__

class Foo:
    def __delitem__(self, key):
        print(key)


obj = Foo()
del obj['name']
複製代碼

刪除分片和上面是同樣的。

__iter__

當 for 循環一個對象時,實際上就是執行類中的 __iter__ 方法。也就是說若是一個對象能夠被 for 進行循環,那麼類中就必須存在 __iter__ 方法。不存在時會報錯。

>>> class Foo:
    def __iter__(self):
        yield 1
        yield 2
        yield 3

obj = Foo()
for i in obj:
    print(i)

1
2
3
複製代碼

__metaclass__

對象默認都是由 type 建立的,咱們卻能夠經過 __metaclass__ 指定該對象有什麼建立。

class Foo:
    __metaclass__ = xxx
複製代碼

表示指定該類由 xxx 建立。

__missing__

須要傳遞進來一個 key,可是沒有傳遞時觸發。

__reversed__

反向迭代,當對對象使用 reversed() 內置方法時觸發。

class Countdown:
    def __init__(self, start):
        self.start = start

    # Forward iterator
    def __iter__(self):
        n = self.start
        while n > 0:
            yield n
            n -= 1

    # Reverse iterator
    def __reversed__(self):
        n = 1
        while n <= self.start:
            yield n
            n += 1

for rr in reversed(Countdown(30)):
    print(rr)
for rr in Countdown(30):
    print(rr)
複製代碼

運算符重載

+, -, *, / 這樣的用於數學計算的字符就屬於運算符,而這些運算符實際上是對應類中的以雙下滑先開頭雙下劃線結尾的方法。

Python 中的運算符有不少,它們分爲:

  • 算術運算符
  • 比較(關係)運算符
  • 賦值運算符
  • 邏輯運算符
  • 位運算符
  • 成員運算符
  • 身份運算符
  • 運算符優先級

Python 中「身份運算符」、「邏輯運算符」和「賦值運算符」以外的全部運算符均可以重載,那運算符對應的類方法是什麼呢?咱們知道 int 類型支持全部的算術運算,所以咱們 help 它一下就知道大多數的運算符對應哪些方法了。而成員運算符就能夠找 list 類。

算術運算:

+ -> __add__
- -> __sub__
* -> __mul__
/ -> __truediv__
% -> __mod__
複製代碼

位運算:

& -> __and__
| -> __or__
複製代碼

比較:

> -> __gt__
< -> __lt__
<= -> __le__
!= -> __ne__
== -> __eq__
複製代碼

成員:

in -> __contains__
索引取值 -> __getitem__
複製代碼

運算符重載就是咱們重寫了一個類的運算符方法。運算符方法在類建立的那一刻就從 object 中繼承了,根據類繼承的原則,咱們在類中從新定義運算符方法,天然就會覆蓋父類中的方法。

重載加法

先定義一個類:

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

>>> a = Point(2, 4)
>>> b = Point(3, 5)
>>> a + b
Traceback (most recent call last):
  File "/usr/local/python3/lib/python3.6/site-packages/IPython/core/interactiveshell.py", line 2862, in run_code
    exec(code_obj, self.user_global_ns, self.user_ns)
  File "<ipython-input-7-f96fb8f649b6>", line 1, in <module>
    a + b
TypeError: unsupported operand type(s) for +: 'Point' and 'Point'
複製代碼

很顯然 a 和 b 並不能相加,可是咱們能夠定義一個方法讓它們實現相加。

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    
    # 定義一個 add 方法
    def add(self, other):
        return Point(self.x + other.x, self.y + other.y)

>>> a = Point(2, 4)
>>> b = Point(3, 5)
>>> c = a.add(b)
>>> c.x
Out[6]: 5
複製代碼

經過一個 add 方法,咱們實現了它們的相加功能。可是,咱們仍是習慣使用加號,事實上,咱們只要改下函數名就可使用 + 進行運算了。

def __add__(self, other):
        return Point(self.x + other.x, self.y + other.y)
複製代碼

很顯然 + 就是調用類的 __add__ 方法,由於咱們只要加入這個方法就可以實現加法操做。

修改運算符

咱們先重載減法的運算符:

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __add__(self, other):
        return Point(self.x + other.x, self.y + other.y)

    def __sub__(self, other):
        return Point(self.x - other.x, self.y - other.y)
複製代碼

而後將加法的默認操做改成減法的:

Point.__add__ = lambda self, value: self - value
複製代碼

這樣一來,咱們執行加法操做,實際上執行的倒是減法:

>>> (Point(8, 9) + Point(2, 4)).x
Out[34]: 6
複製代碼

可是 Python 限制不能對默認類型這麼作。

>>> int.__add__ = lambda self, value: self - value
Traceback (most recent call last):
  File "/usr/local/python3/lib/python3.6/site-packages/IPython/core/interactiveshell.py", line 2862, in run_code
    exec(code_obj, self.user_global_ns, self.user_ns)
  File "<ipython-input-35-77f6fd9e3d43>", line 1, in <module>
    int.__add__ = lambda self, value: self - value
TypeError: can't set attributes of built-in/extension type 'int'
複製代碼

所以不要過分使用運算符重載。

上下文管理

在打開文件的時候,咱們可使用 with 語法,只要出了這個代碼塊,那麼文件會自動關閉,它實際上就是使用了上下文管理。open 方法裏面實現了 __enter____exit__ 這兩個方法,而存在這兩個方法的對象就是支持上下文管理的對象。

咱們定義一個類:

>>> class Context:
...     def __enter__(self):
...         print('enter context')
... 
...     def __exit__(self, *args, **kwargs):
...         print('exit context')
...         
>>> with Context():
...     print('do somethings')
...     
enter context
do somethings
exit context
複製代碼

一個支持上下文管理的對象就能夠經過 with 語句進行管理,在執行 with 代碼塊中的內容以前(__enter__)和以後(__exit__)會作些事情。

即便 with 語句塊中拋出異常,__enter____exit__ 仍然會執行,所以上下文管理是安全的。

>>> with Context():
...     raise Exception # 直接拋出異常
... 
enter context
exit context
Traceback (most recent call last):
  File "/usr/local/python3/lib/python3.6/site-packages/IPython/core/interactiveshell.py", line 2862, in run_code
    exec(code_obj, self.user_global_ns, self.user_ns)
  File "<ipython-input-4-63ba5aff5acc>", line 2, in <module>
    raise Exception
Exception
複製代碼

即便在解釋器退出的狀況下,__exit__ 仍然執行:

import sys
with Context():
    sys.exit()
複製代碼

with 語法還支持 as 子句,它會獲取 __enter__ 的返回值,並將其賦值給 as 後面的變量:

>>> class Context:
...     def __enter__(self):
...         print('enter, context')
...         return 'hehe'
... 
...     def __exit__(self, *args, **kwargs):
...         print('exit context')
... 
... with Context() as c:
...     print(c)
... 
enter, context
hehe
exit context
複製代碼

__enter__ 除了 self 以外,不接收任何參數,或者說它接受參數沒有意義。__exit__ 的返回值沒有辦法獲取到,可是若是 with 語句塊中拋出異常,__exit__ 返回 False 時,會向上拋出異常;返回 True 則會屏蔽異常。

__exit__ 能夠接受參數,不過它的參數都是和異常相關的。當 with 代碼塊中拋出異常時,該異常的信息就會被 __exit__ 所獲取。其中第一個參數是異常的類型、第二個就是這個異常的實例、第三個則是 traceback 對象。對於 with 代碼塊中的異常,咱們只能獲取異常信息,而沒法捕獲。事實上當咱們定義 __exit__ 方法時,IDE 自動會補全爲:

def __exit__(self, exc_type, exc_val, exc_tb):
複製代碼

上下文管理的使用場景:凡是要在代碼塊先後插入代碼的場景,這點和裝飾器相似。

  • 資源管理類:申請和回收,包括打開文件、網絡鏈接、數據庫鏈接等;
  • 權限驗證。

contextlib

若是隻想實現上下文管理而不想定義一個類的話,Python 提供了現成的東西:

import contextlib

@contextlib.contextmanager
def context():
    print('enter context') # 初始化部分
    try:
        yield 'hehe' # 至關於 __enter 的返回值
    finally:
        print('exit context') # 清理部分

with context as c:
    print(c)
複製代碼

若是業務邏輯簡單的話,直接使用這種方式就能夠了;可是若是業務複雜的話,仍是使用類來的直接。

with嵌套

首先定義一個支持上下文管理的類:

from socket import socket, AF_INET, SOCK_STREAM

class LazyConnection:
    def __init__(self, address, family=AF_INET, type=SOCK_STREAM):
        self.address = address
        self.family = family
        self.type = type
        self.sock = None

    def __enter__(self):
        if self.sock is not None:
            raise RuntimeError('Already connected')
        self.sock = socket(self.family, self.type)
        self.sock.connect(self.address)
        return self.sock

    def __exit__(self, exc_ty, exc_val, tb):
        self.sock.close()
        self.sock = None
複製代碼

有一個細節問題就是 LazyConnection 類是否容許多個 with 語句來嵌套使用鏈接。 很顯然,上面的定義中一次只能容許一個 socket 鏈接,若是正在使用一個 socket 的時候又重複使用 with 語句, 就會產生一個異常了。不過你能夠像下面這樣修改下上面的實現來解決這個問題:

from socket import socket, AF_INET, SOCK_STREAM

class LazyConnection:
    def __init__(self, address, family=AF_INET, type=SOCK_STREAM):
        self.address = address
        self.family = family
        self.type = type
        self.connections = []

    def __enter__(self):
        sock = socket(self.family, self.type)
        sock.connect(self.address)
        self.connections.append(sock)
        return sock

    def __exit__(self, exc_ty, exc_val, tb):
        self.connections.pop().close()

# Example use
from functools import partial

conn = LazyConnection(('www.python.org', 80))
with conn as s1:
    pass
    with conn as s2:
        pass
        # s1 and s2 are independent sockets
複製代碼

在第二個版本中,LazyConnection 類能夠被看作是某個鏈接工廠。在內部,一個列表被用來構造一個棧。每次 __enter__() 方法執行的時候,它複製建立一個新的鏈接並將其加入到棧裏面。__exit__() 方法簡單的從棧中彈出最後一個鏈接並關閉它。這裏稍微有點難理解,不過它能容許嵌套使用 with 語句建立多個鏈接,就如上面演示的那樣。

在須要管理一些資源好比文件、網絡鏈接和鎖的編程環境中,使用上下文管理器是很廣泛的。這些資源的一個主要特徵是它們必須被手動的關閉或釋放來確保程序的正確運行。例如,若是你請求了一個鎖,那麼你必須確保以後釋放了它,不然就可能產生死鎖。經過實現 __enter__()__exit__() 方法並使用 with 語句能夠很容易的避免這些問題,由於 __exit__() 方法可讓你無需擔憂這些了。

用類來寫裝飾器

前面都是使用函數寫裝飾器,可是因爲類中存在 __call__ 方法,所以讓經過類寫裝飾器成爲了現實。前面也調到過,使用類來寫裝飾器的好處在於,咱們能夠在類中定義不少的方法,這樣就容易的實現邏輯拆分了,這在定義複雜的裝飾器的時候很好用。

這裏使用一個簡單的類裝飾器做爲例子,用來統計一個函數的執行時間。

import datetime
from functools import wraps

class Timeit:
 # 當類要做爲裝飾器的時候,init 只能接受被裝飾的函數這一個參數
    def __init__(self, fn=None): 
        wraps(fn)(self)

    # 做爲裝飾器還得有一個 call 方法,讓其對象可調用
    def __call__(self, *args, **kwargs):
        start = datetime.datetime.now()
        ret = self.__wrapped__(*args, **kwargs)
        cost = datetime.datetime.now() - start
        print(cost)
        return ret

    def __enter__(self):
        self.start = datetime.datetime.now()

    def __exit__(self, exc_type, exc_val, exc_tb):
        cost = datetime.datetime.now()
        print(cost)

@Timeit
def add(x, y):
    x + y

add(2, 4)
複製代碼

下面的兩個方法是上下文管理用的,做用是上這個類不只能夠做爲裝飾器來計算函數的執行時間,還可以經過 with 語句統計代碼塊的執行時間。

wraps 是 functools 模塊提供的功能,它是一個柯里化函數。它的一個參數是包裝的函數,第二個參數是被包裝的函數。在這裏包裝的函數就是被裝飾的函數,而被包裝的函數就是存在 __call__ 方法的對象自己了。wraps 會給被包裝函數增長一個 __wrapped__ 的屬性,實際上就是包裝的函數 fn。事實上咱們直接調用 fn 也是同樣的。

反射

所謂的反射指的是運行時獲取類的信息。事實上,咱們已經接觸了一些反射相關的東西了,好比實例對象的 __dict__ 就是反射的一種體現。

前面講到了,咱們能夠經過對象的 __dict__ 根據屬性名稱來得到屬性值:

>>> class Point:
...     def __init__(self, x, y):
...         self.x = x
...         self.y = y
... 
...     def print(self):
...         print(self.x, self.y)
...         
>>> a = Point(2, 5)
>>> a.__dict__['x']
Out[13]: 2
複製代碼

可是因爲 __dict__ 中沒有方法,所以咱們是沒法這種方式來獲取方法的。這時就輪到 getattr 登場了,它接收三個參數,分別爲對象、成員名稱和默認值。

>>> getattr(a, 'print')()
2 5
複製代碼

它不光能夠獲取方法,也能獲取屬性:

>>> getattr(a, 'x')
Out[15]: 2
複製代碼

由此,咱們也能知道 setattr 和 hasattr 的用法了。

>>> setattr(a, 'z', 'hehe')
>>> a.z
Out[17]: 'hehe'
複製代碼

setattr 的對象是實例,若是想給實例動態的增長方法首先要將函數轉換成方法,轉化的方法是 type.MethodType。

import types

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def print(self):
        print(self.x, self.y)

def mm(self):
    print(self.x)

p = Point(2, 4)
setattr(p, 'mm', types.MethodType(mm, p))
p.mm()
複製代碼

在描述器中會提到 types.MethodType 是如何實現的。可是,基本上沒有動態給實例增長方法的需求。setattr 通常用來修改實例中已經存在的屬性,不到萬不得已,是不會給實例增減屬性的。

getattr 和 setattr 是相互對立的,一個是獲取一個是設置。hasattr 則用來判斷對象中是否存在特定的成員,它返回一個布爾值。

這三個 *attr 就構成了反射,如下是它應用的一個小示例:

class Command:
    def cmd1(self):
        print('cmd1')

    def cmd2(self):
        print('cmd2')

    def run(self):
        while True:
            cmd = input('>>> ')
            if cmd == 'quit':
                return
            getattr(self, cmd, lambda :print('not found cmd {}'.format(cmd)))()

cmd = Command()
cmd.run()
複製代碼

run 這個函數咱們能夠始終不變,若是想添加功能就定義函數,而後外部就能夠經過字符串的方式直接調用這個方法了。

這只是一個小而簡單的例子,在 RPC 的使用場景中,基本都會用到反射。

描述器

當一個類成員實現了 __get____set__ 方法以後,訪問這個類成員會調用 __get__ 方法,對這個類變量賦值會調用 __set__ 方法。對於實現了這兩種方法的類變量,咱們稱之爲描述器。

描述器是一個類,實現了 __get____set____delete__ 中一個或多個方法。

示例:

class Int:
    def __init__(self, name):
        self.name = name
        self.data = {}

    def __get__(self, instance, owner):
        print('get {}'.format(self.name))
        if instance is not None:
            return self.data[instance]
        return self

    def __set__(self, instance, value):
        self.data[instance] = value

    def __str__(self):
        return 'Int'

    def __repr__(self):
        return 'Int'

class A:
    val = Int('val') # 很顯然 val 是類變量
    def __init__(self):
        self.val = 3

>>> a = A() 
>>> a.val
get val
Out[5]: 3
>>> a.__dict__
Out[6]: {'val': 3}
複製代碼

當對 A 進行實例化的時候,首先會執行 A 中的初始化方法,因爲初始化中有賦值操做,所以裏面的賦值操做不會執行,而是調用 Int 類中的 __set__ 方法(它接收的 instance 是 A 的實例化對象,value 是賦的值),而且在 __set__ 方法執行完畢後,繼續執行初始化操做。而當咱們對 a 中的 val 進行訪問時,會調用 Int 中的 __get__ 方法,它會接收兩個參數,instance 是 A 的實例化對象,cls 爲 A 這個類自己。它是自動傳遞的。

前面提到過實例成員的查找順序,最早查找的是類自己的 __dict__。可是下面卻不是這樣的:

>>> a.__dict__['val'] = 5
>>> a.val
get val
Out[15]: 3 # 結果仍是 3
複製代碼

這是由於帶 __set____delete__ 方法的描述器會提高優先級到 __dict__ 以前。

>>> class Desc:
...     def __get__(self, instance, owner):
...         pass
... 
...     def __delete__(self, instance):
...         pass
... 
... 
... class A:
...     x = Desc()
...     
>>> a = A()
>>> a.x = 3
Traceback (most recent call last):
  File "/usr/local/python3/lib/python3.6/site-packages/IPython/core/interactiveshell.py", line 2862, in run_code
    exec(code_obj, self.user_global_ns, self.user_ns)
  File "<ipython-input-7-0a024de0ab56>", line 1, in <module>
    a.x = 3
AttributeError: __set__
複製代碼

這就是由於 delete 提高了描述器的優先級,所以賦值的時候會先找 set,找不到的話就報錯了。

描述器事實上是一個代理機制,當一個類變量被定義爲描述器,對這個類變量的操做,將由描述器代理。其中:

  • 訪問對應 __get__
  • 賦值對應 __set__
  • 刪除對應 __delete__

注意,沒有增長方法。

即便 __set__ 會提高優先級,可是依然遜於 __getattribute__

以前能夠看到描述器中會接收參數。其中:

__get__(self, instance, owner)
__set__(self, instance, value)
__delete__(self, instance)
複製代碼

instance 表示訪問這個類的實例,owner 表示訪問這個類的類自己。當經過類來訪問時,instance 爲 None;value 表示咱們所賦的值。

實現classmethod

from functools import partial

class Classmethod:
    def __init__(self, fn):
        self.fn = fn

    def __get__(self, instance, owner):
        return partial(self.fn, owner) # 執行 self.fn,並將 owner 做爲參數傳遞給 self.fn

class A:
 @Classmethod # 由於 __get__ 返回的是函數,且函數可調用,所以它能夠做爲裝飾器
    def cls_method(cls):
        print(cls)

A.cls_method() # 經過類調用的話,__get__ 方法的 instance 爲 None
A().cls_method()
複製代碼

partial 做用是將 self.fn 的第一個參數固定爲 owner。cls_method 整個函數包括參數都傳遞給 Classmethod 的構造函數,等到下面經過類訪問類成員(cls_method)的時候,調用 __get__ 方法。經過 partial 執行 self.fn(也就是下面的 cls_method 方法),而且將第一個參數固定爲 owner,而很顯然 owner 就是 A 的實例,因而 cls = A 的實例,因而最後將這個實例打印了出來。

若是裝飾器的寫法看的有些費勁,那麼能夠將之轉換爲:

from functools import partial

class Classmethod:
    def __init__(self, fn):
        self.fn = fn

    def __get__(self, instance, owner):
        return partial(self.fn, owner)

class A:
    cls_method = Classmethod(lambda x: print(x))
複製代碼

實現staticmethod

class Staticmethod:
    def __init__(self, fn):
        self.fn = fn
        
    def __get__(self, instance, owner):
        return self.fn
複製代碼

實現property

class Property:
    def __init__(self, fget, fset=None, fdel=None):
        self.fget = fget
        self.fset = fset
        self.fdel = fdel

    def __get__(self, instance, owner):
        if instance is not None:
            return self.fget(instance)
        return self

    def __set__(self, instance, value):
        if callable(self.fset):
            self.fset(instance, value)
        else:
            raise AttributeError('{} cannot assignable'.format(self.fget.__name__))

    def __delete__(self, instance):
        if callable(self.fdel):
            self.fdel(instance)
        else:
            raise AttributeError('{} cannot deletable'.format(self.fget.__name__))

    def setter(self, fn):
        self.fset = fn
        return self

    def deletter(self, fn):
        self.fdel = fn
        return self


class A:
    def __init__(self):
        self.__x = 1

 @Property
    def x(self):
        return self.__x

 @x.setter
    def x(self, value):
        self.__x = value

 @x.deletter
    def x(self):
        print('cannot delete')
複製代碼

緩存

class lazyproperty:
    def __init__(self, func):
        self.func = func

    def __get__(self, instance, cls):
        if instance is None:
            return self
        else:
            value = self.func(instance)
            setattr(instance, self.func.__name__, value) # 直接將類方法變成了實例方法,下次訪問就不會觸發 __get__ 方法了,這就實現了緩存
            return value

import math

class Circle:
    def __init__(self, radius):
        self.radius = radius

 @lazyproperty # 經過裝飾器將 area 變成了類方法,area = lazyproperty(area)
    def area(self):
        print('Computing area')
        return math.pi * self.radius ** 2

 @lazyproperty
    def perimeter(self):
        print('Computing perimeter')
        return 2 * math.pi * self.radius
複製代碼

下面在一個交互環境中演示它的使用:

>>> c = Circle(4.0)
>>> c.radius
4.0
>>> c.area
Computing area
50.26548245743669
>>> c.area
50.26548245743669
>>> c.perimeter
Computing perimeter
25.132741228718345
>>> c.perimeter
25.132741228718345
複製代碼

仔細觀察你會發現消息 Computing area 和 Computing perimeter 僅僅出現一次。

因爲經過裝飾器將類的動態方法變成了類方法,所以訪問 area 時就觸發了 lazyproperty 類的 __get__ 方法,只是在執行 __get__ 方法的過程當中又將這個類方法變成了實例方法,所以下次再訪問這個實例方法時,就不會再觸發 __get__,也就實現了緩存的效果。能夠經過下面的代碼來觀察它的執行:

>>> c = Circle(4.0)
>>> # Get instance variables
>>> vars(c)
{'radius': 4.0}

>>> # Compute area and observe variables afterward
>>> c.area
Computing area
50.26548245743669
>>> vars(c)
{'area': 50.26548245743669, 'radius': 4.0}

>>> # Notice access doesn't invoke property anymore
>>> c.area
50.26548245743669

>>> # Delete the variable and see property trigger again
>>> del c.area
>>> vars(c)
{'radius': 4.0}
>>> c.area
Computing area
50.26548245743669
複製代碼

這種方案有一個小缺陷就是計算出的值被建立後是能夠被修改的。例如:

>>> c.area
Computing area
50.26548245743669
>>> c.area = 25
>>> c.area
25****
複製代碼

若是你擔憂這個問題,那麼可使用一種稍微沒那麼高效的實現,就像下面這樣:

def lazyproperty(func):
    name = '_lazy_' + func.__name__
 @property
    def lazy(self):
        if hasattr(self, name):
            return getattr(self, name)
        else:
            value = func(self)
            setattr(self, name, value)
            return value
    return lazy

class Circle:
    def __init__(self, radius):
        self.radius = radius

 @lazyproperty
    def area(self): # 結果就是 area = lazyproperty(area),它的結果是一個屬性而不是方法,由於 lazy 被使用 property 了
        print('Computing area')
        return math.pi * self.radius ** 2

 @lazyproperty
    def perimeter(self):
        print('Computing perimeter')
        return 2 * math.pi * self.radius

>>> c = Circle(4.0)
>>> c.area
Computing area
50.26548245743669
複製代碼

經過裝飾器將動態方法 area 變成了類方法 area = lazyproperty(area),並將 area 的參數 self 傳遞給了 lazy。lazyproperty 函數返回的是一個函數,一個被 property 裝飾的函數,也就是能夠經過訪問屬性的方式訪問一個函數。當下面經過訪問屬性的方式執行 c.area 時,開始執行 lazy 函數。

self 中確定沒有 name 屬性存在的,所以執行 else 子句,而後在 else 子句中在 self 中設置了 name 屬性,所以下次訪問就不會執行 else 子句了。因爲 area 是一個 property 對象,所以沒法對其進行賦值。

這種方案有一個缺點就是全部 get 操做都必須被定向到屬性的 getter 函數上去。這個跟以前簡單的在實例字典中查找值的方案相比效率要低一點。

描述器總結

描述器的使用場景爲:用於接管實例變量的操做。好比數據校驗,如下是驗證使用類型註解以後,輸入的類型必須是類型註解的類型。

import inspect

class Typed:
    def __init__(self, name, type):
        self.name = name
        self.type = type

    def __get__(self, instance, owner):
        if instance is not None:
            return instance.__dict__[self.name]
        return self

    def __set__(self, instance, value):
        if not isinstance(value, self.type):
            raise TypeError()
        instance.__dict__[self.name] = value

def typeassert(cls):
    params = inspect.signature(cls).parameters
    for name, param in params.items():
        if param.annotation != inspect._empty:
            setattr(cls, name, Typed(name, param.annotation))
    return cls

@typeassert
class Person:
    def __init__(self, name: str, age: int, desc):
        self.name = name
        self.age = age
        self.desc = desc

Person(11, 'tom', {})
複製代碼

當咱們須要在裝飾器中注入當前類的實例時:

import datetime
from functools import wraps
from types import MethodType

class Timeit:
    def __init__(self, fn):
        self.fn = fn
    
    def __get__(self, instance, owner):
 @wraps(self.fn)
        def wrap(*args, **kwargs):
            start = datetime.datetime.now()
            ret = self.fn(*args, **kwargs)
            cost = datetime.datetime.now() - start
            instance.send(cost)
            return ret
        
        if instance is not None:
            return MethodType(wrap, instance)
        return self

class Sender:
    def send(self, cost):
        print(cost)
    
 @Timeit
    def other(self):
        pass
複製代碼

類的建立與銷燬

前面提到了 __init__ 方法,可是它並不會建立類,它只是執行了初始化。那類是如何建立的,self 又是哪裏來的呢?它來自於 __new__ 方法。

new 方法的定義有必定格式上的要求:

>>> class A:
...     def __new__(cls, *args, **kwargs):
...         print('new')
...         return object.__new__(cls)
... 
... A()
new
Out[2]: <__main__.A at 0x7f2247eeb9b0>
複製代碼

object.__new__() 方法用於給類建立一個對象:

>>> object.__new__(A)
Out[3]: <__main__.A at 0x7f2246b7c240>
複製代碼

__del__ 在對象的生命週期的結束會被調用:

>>> class A:
...     def __new__(cls, *args, **kwargs):
...         print('new')
...         return object.__new__(cls)
... 
...     def __init__(self):
...         print('init')
... 
...     def __del__(self):
...         print('del')
... 

>>> a = A()
new
init
>>> del a
del
複製代碼

元編程

在元組的時候提到過命名元祖:

from collections import namedtuple
Person = namedtuple('Person', ['name', 'age'])
複製代碼

它的神奇之處在於用代碼建立了一個新的數據類型,也就是說代碼具備寫代碼的能力,這種能力叫作元編程。

咱們都知道使用內置的 type 方法能夠檢測一個對象的數據類型,可是它還能夠用來建立一個對象。

>>> class A:
...     pass
... 
>>> type(A) # A 既是類也是對象,它的類型爲 type
Out[9]: type
>>> type('Group', (), {}) # 這就動態的建立了一個對象。
Out[10]: __main__.Group
複製代碼

經過元編程,咱們能夠控制類(注意是類而不是對象)的建立過程。而類的建立過程無非就是:

  • 成員
  • 繼承列表

改變類的建立過程無非就是更改爲員以及繼承列表,這種改變可分爲靜態的方式和動態的方式。首先看靜態的繼承:

>>> class DisplayMixin:
...     def display(self):
...         print(self)
... 
... class A(DisplayMixin):
...     pass
... 
... 
>>> A().display()
<__main__.A object at 0x7f2246b7c160>
複製代碼

而元編程能夠實現動態的方式:

>>> B = type('B', (DisplayMixin, object), {})
>>> B().display() # B 的對象也具備了 display 的方法
<__main__.B object at 0x7f2246b800f0>
複製代碼

咱們能夠將之用在 if 判斷中:

if debug:
    B = type('B', (DisplayMixin, object), {})
else:
    B = type('B', (), {})
複製代碼

若是是 debug 模式,就讓類繼承 display 方法,不然就不繼承。

固然若是以爲這麼寫麻煩的話,還能夠這樣:

class DisplayMixin:
    def display(self):
        print(self)

# 這個類能夠隨意的修改
class Meta(type):
    def __new__(cls, name, bases, clsdict):
        new_bases = [DisplayMixin]
        new_bases.extend(bases)
        return super().__new__(cls, name, tuple(new_bases), clsdict)

    def __init__(self, name, bases, clsdict):
        super().__init__(name, bases, clsdict)

# 而後定義類的時候這麼用便可
class C(metaclass=Meta): # metaclass 用來指定使用哪一個 type 的子類來建立該類
    pass

C().display()
複製代碼

除非你明確知道本身在幹什麼,不然不要使用元編程。

新式類和經典類

經典類就是 class Foo: 這種的,什麼都不繼承的。新式類就是以前老是使用的 object,從 object 類中繼承。新式類做爲新的,確定會比老的多出一些功能。那既然有新式類和經典類了,應該使用哪種呢?用新式類。

新式類是 python 2.2 後出現的,新式類徹底兼容經典類,就是在經典類上面增長了新的功能。

class A:
              ^ ^  def save(self): ...
             /   \
            /     \
           /       \
          /         \
      class B     class C:
          ^         ^  def save(self): ...
           \       /
            \     /
             \   /
              \ /
            class D
複製代碼

類B類C都是從類A繼承的,類D則是從類BC中繼承。按理來講D會繼承C中的save方法,可是經典類中會先找B,B找不到會找A,就不會找C了。示例以下:

class A:
    def __init__(self):
        print 'This is A'
    def save(self):
        print 'save method from A'

class B(A):
    def __init__(self):
        print 'This is B'


class C(A):
    def __init__(self):
        print 'This is C'
    def save(self):
        print 'save method from C'

class D(B,C):
    def __init__(self):
        print 'This is D'

c = D()
c.save()
複製代碼

執行結果爲:

This is D
save method from A
複製代碼

很顯然沒有繼承到C的,而是繼承了A。這是經典類中的BUG,所謂的深度優先。

可是一旦A繼承了新式類結果就是咱們想要的了:

class A(object): # 只加了這點內容
    def __init__(self):
        print 'This is A'
    def save(self):
        print 'save method from A'

class B(A):
    def __init__(self):
        print 'This is B'


class C(A):
    def __init__(self):
        print 'This is C'
    def save(self):
        print 'save method from C'

class D(B,C):
    def __init__(self):
        print 'This is D'

c = D()
c.save()
複製代碼

執行結果:

This is D
save method from C
複製代碼

這就從C中繼承了。

補充

實現contextlib.contextmanager

from functools import wraps

class ContextManager:
    def __init__(self, fn, *args, **kwargs):
        self.gen = fn(*args, **kwargs)

    def __enter__(self):
        return next(self.gen)

    def __exit__(self, exc_type, exc_val, exc_tb):
        try:
            return next(self.gen)
        except StopIteration as e:
            return False

# 使用這個函數是爲了讓使用help查看的時候能顯示正確的文檔信息
# 包括正確的顯示函數名,而由於有了這個函數,因此類中不須要call方法了
def contextmanager(fn):
 @wraps(fn)
    def wrap(*args, **kwargs):
        return ContextManager(fn, *args, **kwargs)
    return wrap
複製代碼

實現super

最簡單的一種形式:

from types import MethodType

class Super:
    def __init__(self, obj):
        self.type = type
        self.obj = obj

    def __getattr__(self, name):
        is_super = False
        for cls in self.type.__mro__:
            if is_super and hasattr(cls, name):
                return MethodType(getattr(cls, name), self.obj)
            if cls == self.type:
                is_super = True
        raise AttributeError()
複製代碼

建立獨一無二的對象

object 是全部類的基類,所以咱們能夠調用它來建立一個對象,這個對象沒什麼實際用處,由於它並無任何有用的方法,也沒有任何實例數據。由於它沒有任何的實例字典,你甚至都不能設置任何屬性值,它惟一的做用就是來標識一個獨一無二的對象。

_no_value = object()

def spam(a, b=_no_value):
    if b is _no_value:
        print('No b value supplied')
複製代碼

經過這個對象來判斷是否有參數傳遞進來。

實踐

cookbook 書中內容。

簡化數據結構的初始化

若是多個類都要進行初始化,且初始化的內容相同的話,就能夠將這個初始化函數獨立出來,造成一個基類。

import math

class Structure1:
    # Class variable that specifies expected fields
    _fields = []

    def __init__(self, *args):
        if len(args) != len(self._fields):
            raise TypeError('Expected {} arguments'.format(len(self._fields)))
        # Set the arguments
        for name, value in zip(self._fields, args): # zip 絕對是很妙的用法
            setattr(self, name, value) # 經過 setattr 設置實例變量,很顯然這個變量是給子類設置的,由於初始化是在子類上完成的
複製代碼

而後使你的類繼承自這個基類:

# Example class definitions
class Stock(Structure1):
    _fields = ['name', 'shares', 'price']

class Point(Structure1):
    _fields = ['x', 'y']

class Circle(Structure1):
    _fields = ['radius']

    def area(self):
        return math.pi * self.radius ** 2
複製代碼

使用這些類的示例:

>>> s = Stock('ACME', 50, 91.1)
>>> p = Point(2, 3)
>>> c = Circle(4.5)
>>> s2 = Stock('ACME', 50)
Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
    File "structure.py", line 6, in __init__
        raise TypeError('Expected {} arguments'.format(len(self._fields)))
TypeError: Expected 3 arguments
複製代碼

若是還想支持關鍵字參數,能夠將關鍵字參數設置爲實例屬性:

class Structure2:
    _fields = []

    def __init__(self, *args, **kwargs):
        if len(args) > len(self._fields):
            raise TypeError('Expected {} arguments'.format(len(self._fields)))

        # Set all of the positional arguments
        for name, value in zip(self._fields, args):
            setattr(self, name, value)

        # Set the remaining keyword arguments
        for name in self._fields[len(args):]:
            setattr(self, name, kwargs.pop(name))

        # Check for any remaining unknown arguments
        if kwargs:
            raise TypeError('Invalid argument(s): {}'.format(','.join(kwargs))) # join 至關於對 kwargs 進行循環,全部值爲 key
# Example use
if __name__ == '__main__':
    class Stock(Structure2):
        _fields = ['name', 'shares', 'price']

    s1 = Stock('ACME', 50, 91.1)
    s2 = Stock('ACME', 50, price=91.1)
    s3 = Stock('ACME', shares=50, price=91.1)
    # s3 = Stock('ACME', shares=50, price=91.1, aa=1)
複製代碼

你還能將不在 _fields 中的名稱加入到屬性中去:

class Structure3:
    # Class variable that specifies expected fields
    _fields = []

    def __init__(self, *args, **kwargs):
        if len(args) != len(self._fields):
            raise TypeError('Expected {} arguments'.format(len(self._fields)))

        # Set the arguments
        for name, value in zip(self._fields, args):
            setattr(self, name, value)

        # Set the additional arguments (if any)
        extra_args = kwargs.keys() - self._fields # 計算差集,返回值爲集合
        for name in extra_args:
            setattr(self, name, kwargs.pop(name))

        if kwargs:
            raise TypeError('Duplicate values for {}'.format(','.join(kwargs)))

# Example use
if __name__ == '__main__':
    class Stock(Structure3):
        _fields = ['name', 'shares', 'price']

    s1 = Stock('ACME', 50, 91.1)
    s2 = Stock('ACME', 50, 91.1, date='8/2/2012')
複製代碼

當你須要使用大量很小的數據結構類的時候,相比手工一個個定義 __init__() 方法而已,使用這種方式能夠大大簡化代碼。在上面的實現中咱們使用了 setattr() 函數類設置屬性值,你可能不想用這種方式,而是想直接更新實例字典,就像下面這樣:

class Structure:
    # Class variable that specifies expected fields
    _fields= []
    def __init__(self, *args):
        if len(args) != len(self._fields):
            raise TypeError('Expected {} arguments'.format(len(self._fields)))

        # Set the arguments (alternate)
        self.__dict__.update(zip(self._fields,args))
複製代碼

儘管這也能夠正常工做,可是當定義子類的時候問題就來了。當一個子類定義了 __slots__ 或者經過 property(或描述器)來包裝某個屬性,那麼直接訪問實例字典就不起做用了。咱們上面使用 setattr() 會顯得更通用些,由於它也適用於子類狀況。

這種方法惟一很差的地方就是對某些IDE而言,在顯示幫助函數時可能不太友好。好比:

>>> help(Stock)
Help on class Stock in module __main__:
class Stock(Structure)
...
| Methods inherited from Structure:
|
| __init__(self, *args, **kwargs)
|
...
複製代碼
相關文章
相關標籤/搜索