面向對象進階
類型判斷
issubclass
首先,我們先看issubclass() 這個內置函數可以幫我們判斷x類是否是y類型的子類。
class Base: pass class Foo(Base): pass class Bar(Foo): pass print(issubclass(Bar, Foo)) # True print(issubclass(Foo, Bar)) # False print(issubclass(Bar, Base)) # True
type
然后我們來看type,type在前面的學習期間已經使用過了。type(obj) 表示查看obj是由哪個類創建的。
class Foo: pass obj = Foo() print(obj, type(obj)) # 查看obj的類
isinstance
isinstance也可以判斷x是y類型的數據。
class Base: pass class Foo(Base): pass class Bar(Foo): pass print(isinstance(Foo(), Foo)) # True print(isinstance(Foo(), Base)) # True print(isinstance(Foo(), Bar)) # False
isinstance可以判斷該對象是否是家族體系中的(只能往上判斷類)。
反射
為什么需要反射?
首先,我們來看這樣一個需求。
從前有一個大牛,寫了一堆特別牛B的代碼。然后放在一個py文件(模塊)中。這個時候你想使用一下大牛寫的東西。但是呢,你首先得知道大牛寫的這些代碼都是干什么用的。那就需要你把大牛寫的每一個函數跑一下。
大牛.py
def chi(): print("⼤⽜一頓吃100個螃蟹") def he(): print("⼤牛一頓喝100瓶可樂") def la(): print("⼤牛不⽤拉") def shui(): print("⼤牛⼀次睡一年")
接下來,到你了。你要去一個一個調用。但是在你調用之前,大牛告訴你,他寫了哪些方法,那現在就可以這么辦了:
while 1: print(''' 作為大牛,我幫你寫了 chi he la shui 等功能,你自己看着辦吧... ''') func = input('請輸入你要測試的功能:').strip() if func == 'chi': daniu.chi() elif func == 'he': daniu.he() elif func == 'la': daniu.la() elif func == 'shui': daniu.shui() else: print('大牛就這幾個功能,別搞事情!')
這樣寫是寫完了,但是...
如果大牛寫了100個功能怎么辦,你要寫100個if判斷嗎?太累了吧,現有的知識解決不了這個問題呀,那怎么辦?
我們可以使用反射來完成這樣的功能,非常簡單,我們現在可以獲取要執行的功能,只不過我們使用input()函數獲取的一個字符串,這個字符串和實際模塊中的函數名是一樣的。那我們就可以利用這一點,只要通過字符串動態的訪問模塊中的功能就可以了,反射就是做這個事情的。
什么是反射?
之前我們導入模塊都是,先引入模塊,然后通過模塊去訪問否個我們要用的功能,現在呢?我們手動輸入要運行的功能,然后拿着這個功能去模塊里查找,這就叫反射。
通俗點說就是通過字符串的形式操作對象相關的屬性。Python中的一切事物都是對象(都可以使用反射)
我們首先來看一下,在Python中使用反射如何解決上面的問題吧。
import daniu while 1: print(''' 作為大牛,我幫你寫了 chi he la shui 等功能,你自己看着辦吧... ''') func_str = input('請輸入你要測試的功能:').strip() if hasattr(daniu, func_str): func = getattr(daniu, func_str) func() else: print('大牛就這幾個功能,別搞事情!')
上面的代碼中用到了如下兩個方法:
hasattr(對象, 字符串)是用來判斷對象是否有字符串名稱對應的這個屬性(功能)。
getattr(對象,字符串)是用來獲取對象中字符串名稱對應的屬性(功能)。
因為Python中一切皆對象,所以反射這個特性非常的有用,很多框架中都會用到此特性。
反射應用
接下來,我們先看個簡單的例子:
class Person: country = "China" def eat(self): pass # 類中的內容可以這樣動態的進⾏獲取 print(getattr(Person, "country")) print(getattr(Person, "eat")) # 相當於Foo.func 函數 # 對象⼀樣可以 obj = Person() print(getattr(obj, "country")) print(getattr(obj, "eat")) # 相當於obj.func ⽅法
getattr可以從模塊中獲取內容,也可以從類中獲取內容,也可以從對象中獲取內容。又因為在Python中⼀切皆為對象,所以把反射理解為從對象中動態的獲取成員。
反射的四個函數
關於反射, 其實⼀共有4個函數:
- hasattr(obj, str) 判斷obj中是否包含str成員
- getattr(obj,str) 從obj中獲取str成員。
- setattr(obj, str, value) 把obj中的str成員設置成value。這⾥的value可以是值,也可以是函數或者⽅法。
- delattr(obj, str) 把obj中的str成員刪除掉。
class Foo: pass f = Foo() print(hasattr(f, 'eat')) # False setattr(f, 'eat', "123") print(f.eat) # 被添加了⼀個屬性信息 setattr(f, "eat", lambda x: x + 1) print(f.eat(3)) # 4 print(f.eat) # 此時的eat既不是靜態⽅法, 也不是實例⽅法, 更不是類⽅法. 就相當於你在類中寫了個self.chi = lambda 是⼀樣的 print(f.__dict__) # {'eat': <function <lambda> at 0x1015a2048>} delattr(f, "eat") print(hasattr(f, "eat")) # False
注意:以上操作都是在內存中進⾏的,並不會影響你的源代碼。
補充importlib
importlib是一個可以根據字符串的模塊名實現動態導入模塊的庫。
舉個例子:
目錄結構:
├── aaa.py
├── bbb.py
└── mypackage
├── __init__.py
└── xxx.py
使用importlib動態導入模塊:
bbb.py
import importlib func = importlib.import_module('aaa') print(func) func.f1() m = importlib.import_module('mypackage.xxx') print(m.age)
類的其他成員
列舉類中的其他常見成員。
__str__
改變對象的字符串顯示。可以理解為使用print函數打印一個對象時,會自動調用對象的__str__方法。
class Student: def __init__(self, name, age): self.name = name self.age = age # 定義對象的字符串表示 def __str__(self): return self.name s1 = Student('張三', 24) print(s1) # 會調用s1的__str__方法
__repr__
在python解釋器環境下,會默認顯示對象的repr表示。
>>> class Student: ... def __init__(self, name, age): ... self.name = name ... self.age = age ... def __repr__(self): ... return self.name ... >>> s1 = Student('張三', 24) >>> s1 張三
總結:
str函數或者print函數調用的是obj.__str__()
repr函數或者交互式解釋器調用的是obj.__repr__()
注意:
如果__str__沒有被定義,那么就會使用__repr__來代替輸出。
__str__和__repr__方法的返回值都必須是字符串。
__format__

class Student: def __init__(self, name, age): self.name = name self.age = age # 定義對象的字符串表示 def __str__(self): return self.name def __repr__(self): return self.name __format_dict = { 'n-a': '{obj.name}-{obj.age}', # 姓名-年齡 'n:a': '{obj.name}:{obj.age}', # 姓名:年齡 'n/a': '{obj.name}/{obj.age}', # 姓名/年齡 } def __format__(self, format_spec): """ :param format_spec: n-a,n:a,n/a :return: """ if not format_spec or format_spec not in self.__format_dict: format_spec = 'n-a' fmt = self.__format_dict[format_spec] return fmt.format(obj=self) s1 = Student('張三', 24) ret = format(s1, 'n/a') print(ret) # 張三/24
__del__
析構方法,當對象在內存中被釋放時,自動觸發執行。
注:此方法一般無須定義,因為Python是一門高級語言,程序員在使用時無需關心內存的分配和釋放,因為此工作都是交給Python解釋器來執行,所以析構函數的調用是由解釋器在進行垃圾回收時自動觸發執行的。

class A: def __del__(self): print('刪除了...') a = A() print(a) # <__main__.A object at 0x10164fb00> del a # 刪除了... print(a) # NameError: name 'a' is not defined
__dict__和__slots__
Python中的類,都會從object里繼承一個__dict__屬性,這個屬性中存放着類的屬性和方法對應的鍵值對。一個類實例化之后,這個類的實例也具有這么一個__dict__屬性。但是二者並不相同。
class A: some = 1 def __init__(self, num): self.num = num a = A(10) print(a.__dict__) # {'num': 10} a.age = 10 print(a.__dict__) # {'num': 10, 'age': 10}
從上面的例子可以看出來,實例只保存實例的屬性和方法,類的屬性和方法它是不保存的。正是由於類和實例有__dict__屬性,所以類和實例可以在運行過程動態添加屬性和方法。
但是由於每實例化一個類都要分配一個__dict__變量,容易浪費內存。因此在Python中有一個內置的__slots__屬性。當一個類設置了__slots__屬性后,這個類的__dict__屬性就不存在了(同理,該類的實例也不存在__dict__屬性),如此一來,設置了__slots__屬性的類的屬性,只能是預先設定好的。
當你定義__slots__后,__slots__就會為實例使用一種更加緊湊的內部表示。實例通過一個很小的固定大小的小型數組來構建的,而不是為每個實例都定義一個__dict__字典,在__slots__中列出的屬性名在內部被映射到這個數組的特定索引上。使用__slots__帶來的副作用是我們沒有辦法給實例添加任何新的屬性了。
注意:盡管__slots__看起來是個非常有用的特性,但是除非你十分確切的知道要使用它,否則盡量不要使用它。比如定義了__slots__屬性的類就不支持多繼承。__slots__通常都是作為一種優化工具來使用。--摘自《Python Cookbook》8.4
class A: __slots__ = ['name', 'age'] a1 = A() # print(a1.__dict__) # AttributeError: 'A' object has no attribute '__dict__' a1.name = '張三' a1.age = 24 # a1.hobby = '吹牛逼' # AttributeError: 'A' object has no attribute 'hobby' print(a1.__slots__)
注意事項:
__slots__的很多特性都依賴於普通的基於字典的實現。
另外,定義了__slots__后的類不再 支持一些普通類特性了,比如多繼承。大多數情況下,你應該只在那些經常被使用到的用作數據結構的類上定義__slots__,比如在程序中需要創建某個類的幾百萬個實例對象 。
關於__slots__的一個常見誤區是它可以作為一個封裝工具來防止用戶給實例增加新的屬性。盡管使用__slots__可以達到這樣的目的,但是這個並不是它的初衷。它更多的是用來作為一個內存優化工具。
__item__系列

class Foo: def __init__(self, name): self.name = name def __getitem__(self, item): print(self.__dict__[item]) def __setitem__(self, key, value): self.__dict__[key] = value def __delitem__(self, key): print('del obj[key]時,執行我') self.__dict__.pop(key) def __delattr__(self, item): print('del obj.key時,執行我') self.__dict__.pop(item) f1 = Foo('sb') print(f1.__dict__) f1['age'] = 18 f1.hobby = '吹牛逼' del f1.hobby del f1['age'] f1['name'] = 'alex' print(f1.__dict__)
__init__
使用Python寫面向對象的代碼的時候我們都會習慣性寫一個 __init__ 方法,__init__ 方法通常用在初始化一個類實例的時候。例如:
class Person: def __init__(self, name, age): self.name = name self.age = age def __str__(self): return '<Person: {}({})>'.format(self.name, self.age) p1 = Person('張三', 24) print(p1)
上面是__init__最普通的用法了。但是__init__其實不是實例化一個類的時候第一個被調用的方法。當使用 Persion(name, age) 來實例化一個類時,最先被調用的方法其實是 __new__ 方法。
__new__
其實__init__是在類實例被創建之后調用的,它完成的是類實例的初始化操作,而 __new__方法正是創建這個類實例的方法。
class Person: def __new__(cls, *args, **kwargs): print('調用__new__,創建類實例') return super().__new__(Person) def __init__(self, name, age): print('調用__init__,初始化實例') self.name = name self.age = age def __str__(self): return '<Person: {}({})>'.format(self.name, self.age) p1 = Person('張三', 24) print(p1)
輸出:
調用__new__,創建類實例 調用__init__,初始化實例 <Person: 張三(24)>
__new__方法在類定義中不是必須寫的,如果沒定義的話默認會調用object.__new__去創建一個對象(因為創建類的時候默認繼承的就是object)。
如果我們在類中定義了__new__方法,就是重寫了默認的__new__方法,我們可以借此自定義創建對象的行為。
舉個例子:
重寫類的__new__方法來實現單例模式。
class Singleton: # 重寫__new__方法,實現每一次實例化的時候,返回同一個instance對象 def __new__(cls, *args, **kw): if not hasattr(cls, '_instance'): cls._instance = super().__new__(Singleton) return cls._instance def __init__(self, name, age): self.name = name self.age = age s1 = Singleton('張三', 24) s2 = Singleton('李四', 20) print(s1, s2) # 這兩實例都一樣 print(s1.name, s2.name)
__call__
__call__ 方法的執行是由對象后加括號觸發的,即:對象()。擁有此方法的對象可以像函數一樣被調用。
class Person: def __init__(self, name, age): self.name = name self.age = age def __call__(self, *args, **kwargs): print('調用對象的__call__方法') a = Person('張三', 24) # 類Person可調用 a() # 對象a可以調用
注意:
__new__、__init__、__call__等方法都不是必須寫的。
__doc__
定義類的描述信息。注意該信息無法被繼承。
class A: """我是A類的描述信息""" pass print(A.__doc__)
__iter__和__next__
之前的課程中講過,如果一個對象擁有了__iter__和__next__方法,那這個對象就是可迭代對象。
class A: def __init__(self, start, stop=None): if not stop: start, stop = 0, start self.start = start self.stop = stop def __iter__(self): return self def __next__(self): if self.start >= self.stop: raise StopIteration n = self.start self.start += 1 return n a = A(1, 5) from collections import Iterator print(isinstance(a, Iterator)) for i in A(1, 5): print(i) for i in A(5): print(i)
__enter__和__exit__
一個對象如果實現了__enter__和___exit__方法,那么這個對象就支持上下文管理協議,即with語句。
class A: def __enter__(self): print('進入with語句塊時執行此方法,此方法如果有返回值會賦值給as聲明的變量') def __exit__(self, exc_type, exc_val, exc_tb): """ :param exc_type: 異常類型 :param exc_val: 異常值 :param exc_tb: 追溯信息 :return: """ print('退出with代碼塊時執行此方法') print('1', exc_type) print('2', exc_val) print('3', exc_tb) with A() as f: print('進入with語句塊') # with語句中代碼塊出現異常,則with后的代碼都無法執行。 # raise AttributeError('sb') print('嘿嘿嘿')
上下文管理協議適用於那些進入和退出之后自動執行一些代碼的場景,比如文件、網絡連接、數據庫連接或使用鎖的編碼場景等。
__len__
擁有__len__方法的對象支持len(obj)操作。
class A: def __init__(self): self.x = 1 self.y = 2 def __len__(self): return len(self.__dict__) a = A() print(len(a))
__hash__
擁有__hash__方法的對象支持hash(obj)操作。
class A: def __init__(self): self.x = 1 self.x = 2 def __hash__(self): return hash(str(self.x) + str(self.x)) a = A() print(hash(a))
__eq__
擁有__eq__方法的對象支持相等的比較操作。
class A: def __init__(self): self.x = 1 self.y = 2 def __eq__(self,obj): if self.x == obj.x and self.y == obj.y: return True a = A() b = A() print(a == b)
類的繼承
抽象和繼承
抽象是指找出物體之間的共同點,把這些相同的部分抽象成一個划分標准。
比如黑人和白人之間的共同點是都是人。
抽象最主要的作用是划分類別(可以隔離關注點,降低復雜度),看下圖:
繼承
繼承:是基於抽象的結果,通過編程語言去實現它,肯定是先經歷抽象這個過程,才能通過繼承的方式去表達出抽象的結構。
為什么要使用繼承?
代碼重用
使用繼承的一個目的就是實現代碼的重用。
在開發程序的過程中,如果我們定義了一個A類,然后又想新實現另外一個B類,但是B類的大部分內容與A類相同時我們就沒有必要從頭實現一個B類了,這時就可以使用類的繼承。
通過繼承的方式實現B類,讓B類繼承A類,B類就會‘遺傳’A類的所有屬性和方法(除私有成員外),從而實現代碼重用。
看代碼:
class Animal: """ 人和狗都是動物,所以創造一個Animal基類 """ def __init__(self, name, age): self.name = name # 人和狗都有昵稱; self.age = age # 人和狗都有年齡 def eat(self): print('{}在吃東西'.format(self.name)) # 創建一個Dog類繼承Animal類 class Dog(Animal): pass # 創建一個Person類繼承Animal類 class Person(Animal): pass # 實例化一個人 p1 = Person('張三', 18) # 實例化一條狗 d1 = Dog('二哈', 5) p1.eat() # 人能吃東西 d1.eat() # 狗也能吃東西
當然,我們在子類中還可以定義自己子類獨有的一些屬性和方法。
class Animal: """ 人和狗都是動物,所以創造一個Animal基類 """ def __init__(self, name, age): self.name = name # 人和狗都有昵稱; self.age = age # 人和狗都有年齡 def eat(self): print('{}在吃東西'.format(self.name)) # 創建一個Dog類繼承Animal類 class Dog(Animal): pass # 創建一個Person類繼承Animal類 class Person(Animal): # Person類可以自己定義自己獨有的方法 def dream(self): print('{}在做夢...'.format(self.name)) # 實例化一個人 p1 = Person('張三', 18) # 實例化一條狗 d1 = Dog('二哈', 5) p1.eat() # 人能吃東西 d1.eat() # 狗也能吃東西 p1.dream() # 人能做夢
當然我們還可以重寫父類的方法:
class Animal: """ 人和狗都是動物,所以創造一個Animal基類 """ def __init__(self, name, age): self.name = name # 人和狗都有昵稱; self.age = age # 人和狗都有年齡 def eat(self): print('{}在吃東西'.format(self.name)) # 創建一個Dog類繼承Animal類 class Dog(Animal): # 重寫父類的eat方法,Dog實例以后都使用這個eat方法 def eat(self): print('吃骨頭吃骨頭吃骨頭')
規范類的行為
我們可以聲明一個類,在該類中定義了一些方法名但是並沒有實現具體的功能,等子類繼承我這個類的時候再去實現方法應該有的功能。
舉個例子:
我們要做一個電商網站,在寫支付功能的時候,我們的客戶可以選擇支付寶支付、微信支付、銀聯支付等各種支付方式。那代碼里應該怎么實現這個關系呢?
def check_out(payment, money): """ 一個結賬業務邏輯函數,實現具體的結賬功能。 :param payment: 具體的支付方式實例對象 :param money: 需要支付的金額 :return: """ payment.pay(money) # 調用具體支付方式對象的pay方法
現在我們要動手實現支付寶支付和微信支付,兩名萌萌的程序員就開工了,寫出了下面的代碼:
程序員A:
class AliPay: """支付寶支付""" def __init__(self): # 配置支付寶收款賬號信息 pass # 具體的支付方法 def pay(self, money): print('支付寶支付了{}元'.format(money))
程序員B:
class WeChatPay: """微信支付""" def __init__(self): # 配置微信支付收款賬號信息 pass # 具體的支付方法 def zhifu(self, money): print('使用支付寶支付了{}元'.format(money))
此時此刻,我們就會發現,我們確實需要為某些類定義一些統一的規則或約束。
# 定義一個支付的基類 class Payment: # 規定子類必須實現pay方法,否則調用實例的pay方法時會拋出異常 def pay(self, *args, **kwargs): raise NotImplementedError class AliPay(Payment): """支付寶支付""" def __init__(self): # 配置支付寶收款賬號信息 pass # 實現父類中定義好的支付方法 def pay(self, money): print('支付寶支付了{}元'.format(money)) class WeChatPay(Payment): """微信支付""" def __init__(self): # 配置微信支付收款賬號信息 pass # 具體的支付方法 def pay(self, money): print('使用支付寶支付了{}元'.format(money)) p = WeChatPay() check_out(p, 200)
Python中為了解決上面的問題,已經給我們提供了一套現成的工具。
from abc import ABCMeta, abstractmethod class Payment(metaclass=ABCMeta): # 子類必須實現pay方法,否則實例化子類的時候就會報錯 @abstractmethod def pay(self, money): pass class WeChatPay(Payment): def fuqian(self, money): print('微信支付了{}元'.format(money)) p = WeChatPay() # 實例化時就報錯了
調用父類的方法
我們在子類中想要調用父類的方法,可以使用super()函數來完成。
class A: def m1(self): print('A.m1') class B(A): def m1(self): print('B.m1') super().m1() # 在子類中調用父類中的m1方法 b = B() b.m1()
super()函數的一種常見用途是調用父類的__init__()方法,以確保父類被正確的初始化了。
class A: def __init__(self): self.x = 0 class B(A): def __init__(self): # 先調用父類的__init__()方法 super().__init__() # 執行自己的初始化操作 self.y = 1 b = B() print(b.x) print(b.y)
多繼承的順序
Python中支持多繼承!所以會存在一個繼承順序的問題。
首先我們來看一個經典的鑽石繼承問題:
class A: def __init__(self): print('進入A類') print('離開A類') class B(A): def __init__(self): print('進入B類') super().__init__() print('離開B類') class C(A): def __init__(self): print('進入C類') super().__init__() print('離開C類') class D(B, C): def __init__(self): print('進入D類') super().__init__() print('離開D類')
其中,A是父類,B, C 繼承自A, D繼承自 B和C,它們的繼承關系如下:
我們看一下實例化D的輸出結果:
進入D類
進入B類
進入C類
進入A類
離開A類
離開C類
離開B類
離開D類
你會發現,super()函數並不是簡單的調用父類的方法,因為上面的結果顯示在“進入B類”之后打印的不是 “進入A類”,而是“進入C類”。
看來super()和父類沒什么實質性的關系,那它是按照什么規則來調用方法的呢?
MRO列表
對於我們定義的每一個類,Python會計算出一個方法解析順序(Method Resolution Order, MRO)列表,它代表了類繼承的順序,我們可以使用下面的方式獲得某個類的 MRO 列表:
[<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>]
那這個 MRO 列表的順序是怎么定的呢,它是通過一個 C3 線性化算法來實現的,這里我們就不去深究這個算法了,簡單來說,一個類的 MRO 列表就是合並所有父類的 MRO 列表,並遵循以下三條原則:
- 子類永遠在父類前面
- 如果有多個父類,會根據它們在列表中的順序被檢查
- 如果對下一個類存在兩個合法的選擇,選擇第一個父類
super()函數所做的事情就是在MRO列表中找到當前類的下一個類。