參考原文
PS:數據封裝、繼承和多態只是OOP中最基礎的3個概念。在Python中,面向對象還有很多高級的特性,我們會討論多重繼承、定制類、元類等概念。
動態語言的靈活性
正常情況下,當我們定義了一個class,創建了該類的實例后,我們可以給該實例綁定任何屬性和方法,這就是動態語言的靈活性。先定義一個類:
class Student(object): pass
然后給一個實例綁定一個屬性:
s = Student() s.name = 'Alice' print(s.name) #result Alice
還可以為實例綁定一個方法:
>>> def set_age(self, age): # 定義一個函數作為實例方法 ... self.age = age ... >>> from types import MethodType >>> s.set_age = MethodType(set_age, s) # 給實例綁定一個方法 >>> s.set_age(25) # 調用實例方法 >>> s.age # 測試結果 25
也可以為類動態添加方法使所有實例均可調用:
>>> def set_score(self, score): ... self.score = score ... >>> Student.set_score = set_score >>> s.set_score(100) >>> s.score 100 >>> s2.set_score(99) >>> s2.score 99
但我們不一定想要它“隨心所欲”,如果我們想要限制實例的屬性怎么辦?--使用__slots__。
__slots__
為了達到限制目的,Python允許在定義class的時候,定義一個特殊的變量__slots__來限制該class的實例能添加的屬性、方法。如:
class Student(object): __slots__ = ('name', 'age') # 用tuple定義允許綁定的屬性、方法名稱
試試:
>>> s = Student() >>> s.name = 'Alice' >>> s.score = 12 Traceback (most recent call last): File "<pyshell#4>", line 1, in <module> s.score = 12 AttributeError: 'Student' object has no attribute 'score'
Tips:__slots__定義的屬性僅對當前實例起作用,對繼承的子類是無效的,除非在子類也定義__slots__
@property
我們之前說過,可以將屬性設置成私有的,然后通過一個方法來進行操作該屬性,這樣就可以檢查參數的有效性。如
class Student(object): def get_score(self): return self._score def set_score(self, value): if not isinstance(value, int): raise ValueError('score must be an integer!') if value < 0 or value > 100: raise ValueError('score must between 0 ~ 100!') self._score = value
現在,對任意的Student實例進行操作,就不能隨心所欲地設置score:
>>> s = Student() >>> s.set_score(60) # ok! >>> s.get_score() 60 >>> s.set_score(9999) Traceback (most recent call last): ... ValueError: score must between 0 ~ 100!
但這樣做又難免復雜,不符合Python簡潔的定義,倒和C#這種“嚴格”的語言差不多了,那有沒有既能檢查參數,又可以用類似屬性這樣簡單的方式來訪問累的變量呢?對於追求完美的Python程序員來說,這是必須的!回想下,已經學過的知識,似乎裝飾器(decorator)可以給函數動態加上功能。對於類的方法,裝飾器一樣起作用。
Python內置的@property裝飾器就是負責把一個方法變成屬性調用,如:
class Student(object): @property def score(self): return self._score @score.setter def score(self, value): if not isinstance(value, int): raise ValueError('score must be an integer') if value < 0 or value > 100: raise ValueError('score must between 0 and 100') self._score = value
要把一個方法變成屬性,只需在get方法上加上@property,此時@property本身又自動創建了另一個裝飾器@屬性名.setter,用於對屬性賦值。此時,我們就擁有了一個可控的屬性操作:
>>> s = Student() >>> s.score = 60 #ok,實際調用形如s.set_score(60) >>> s.score #s實際調用形如s.get_score() 60 >>> s.score = 101 Traceback (most recent call last): File "<pyshell#10>", line 1, in <module> s.score = 101 File "<pyshell#1>", line 12, in score raise ValueError('score must between 0 and 100') ValueError: score must between 0 and 100
Tips:當我們看到@property,就應該知道該屬性不是直接進行操作的,而是通過getter、setter方法來實現的,也可以只定義只讀屬性(不定義setter方法 )。
多重繼承
前面已經說過繼承了,通過繼承,子類可以獲得父類的全部功能。但如果子類還想獲得更多的功能怎么辦呢?除了擴展自己的特色方法外,還可以通過多重繼承來獲得多個父類的功能。如:
class Runable(object): def run(self): print('Runnng...') class Eatable(object): def eat(self): print('Eating...') class Dog(Runable, Eatable): pass dog = Dog() dog.run() dog.eat() ''' Runnng... Eating... '''
MixIn
在設計類的繼承關系時,通常主線都是單一繼承下來的,如果要加入額外的功能可以通過多重繼承來實現。讓A繼承A1,同時繼承A2,這種設計通常稱之為MixIn。
這樣就可以把Runable和Eatable改成RunbleMixIn和EatableMixIn了,這樣就更加了然了。在Python中自帶的很多庫也使用了MixIn。舉個例子,Python中自帶了TCPServer和UDPServer這兩種網絡服務,而要同時服務多個用戶就必須使用多進程和多線程模型,這兩種模型分別由ForkingMixIn和ThreadingMixIn提供。通過組合就可以創造出合適的服務出來了,如編寫一個多進程的TCP服務:
class MyTCPServer(TCPServer, ForkingMixIn): pass
多線程的UDP服務:
class MyUDPServer(UDPServer, ThreadingMixIn): pass
Tips:由於Python允許多重繼承,所以MixIn是一種常見的設計,而只允許單一繼承的語言(如Java)不能使用MixIn設計。
定制類
定制類,就是通過一些特殊的方法來使我們的類具有特殊的功能來應對某些特定的場合。前面我們已經說過了一些特殊的變量或函數名(形如__xx__)如__slots__和__len__()。接下來我們就要說一些特殊的方法了。
__str__
為了說明__str__的作用,我們先定義一個Student類,並打印出一個實例:
>>> class Student(object): def __init__(self, name): self.name = name >>> print(Student('Alice')) <__main__.Student object at 0x000001DFB4FC1DA0>
這打印出來的字符串明顯不好看,這時__str__()方法就可以派上用場了:返回一個好看的字符串:
>>> class Student(object): def __init__(self, name): self.name = name def __str__(self): return 'Student object (name: %s)' % self.name >>> print(Student('Alice')) Student object (name: Alice) >>>
這樣打印出來的實例不但好看,而且容易看出實例內部的重要數據。但是在Python解釋器下,直接敲變量打印出來的實例還是和原來一樣不好看:
>>> s = Student('Alice') >>> s <__main__.Student object at 0x000001DFB4FC1D30>
為什么呢?這是因為直接敲變量調用的不是__str()__,而是__repr__(),前者是返回給用戶看的字符串,后者是返回給程序開發者看到的字符串,也就是說__repr__()是為調試服務的。解決的辦法是再定義一個__repr__(),但是通常下__str()__和__repr__()代碼是一樣的,所以可以偷個懶直接使--repr__ = __str__:
class Student(object): def __init__(self, name): self.name = name def __str__(self): return 'Student object (name=%s)' % self.name __repr__ = __str__
__iter__
我們已經知道list或tuple的數據類型可以被用於for ... in ... 循環,那么如果類也想被用於for .. in ... 循環,該怎么辦呢?那就是實現__iter()__方法,該方法返回一個迭代對象,for循環不斷調用該迭代對象的__next()__方法拿到循環的下一個值,知道遇到StopIteration錯誤時退出循環。
我們以斐波那契數為例,寫一個Fib類,用作for循環:
class Fib(object): def __init__(self): self.a, self.b = 0, 1 #初始化兩個計數器a,b def __iter__(self): return self #實例本身就是迭代對象,故返回自己 def __next__(self): self.a, self.b = self.b, self.a + self.b #計算下一個值 if self.a > 100: # 退出循環 raise StopIteration() return self.a #返回下一個值 for n in Fib(): print(n) ''' 1
1 2 3 5 8 13 21 34 55 89 '''
__getitem__
上面的Fib實例雖然能用作for循環了,但把它當成list來使用還是不行的,比如按索引取元素:
>>> Fib()[5] Traceback (most recent call last): File "<pyshell#29>", line 1, in <module> Fib()[5] TypeError: 'Fib' object does not support indexing
此時如果要表現得想lsit那樣按索引取元素,就需要實現__getitem__()方法了:
>>> class Fib(object): def __getitem__(self, n): a, b = 1, 1 for x in range(n): a, b = b, a + b return a >>> Fib()[9] 55
試試list的切片:
>>> Fib()[1:3] Traceback (most recent call last): File "<pyshell#33>", line 1, in <module> Fib()[1:3] File "<pyshell#31>", line 4, in __getitem__ for x in range(n): TypeError: 'slice' object cannot be interpreted as an integer
報錯是因為__getitem__()傳入的參數可能是一個int,也可能是一個切片對象slice所以要做判斷:
class Fib(object): def __getitem__(self, n): if isinstance(n, int): # n是索引 a, b = 1, 1 for x in range(n): a, b = b, a + b return a if isinstance(n, slice): # n是切片 start = n.start stop = n.stop if start is None: start = 0 a, b = 1, 1 L = [] for x in range(stop): if x >= start: L.append(a) a, b = b, a + b return L
再試試切片:
>>> Fib()[0:5]
[1, 1, 2, 3, 5]
但還是卻沒對step參數作處理:
>>> f[:10:2]
[1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89]
所以要實現一個完整的__getitem__()還是要很多工作要做的。
Tips:如果把對象看成一個dict,那么__getitem__()的參數也可能是一個可以作為key的對象如str,與之對應的是__setitem_()方法,把對象視作為list來賦值,還有一個__delitem__(),用於刪除某個元素。所以我們可以通過定義特殊的方法來使自己定義的類表現得和Python自帶的list、tuple、dict一樣(Python動態語言的“鴨子類型”)。
__getattr__
我們知道正常情況下,當調用類不存在的屬性或方法時,就會報錯:
>>> class Student(object): pass >>> s = Student() >>> s.name Traceback (most recent call last): File "<pyshell#40>", line 1, in <module> s.name AttributeError: 'Student' object has no attribute 'name'
我們可以避免這個錯誤,除了加上這屬性外,Python還有另一個機制,那就是實現一個__getattr__()方法,動態返回一個屬性。修改上面的代碼如下:
>>> class Student(object): def __getattr__(self, attr): if attr == 'name': return 'Alice' >>> s = Student() >>> s.name 'Alice'
Tips:也可以返回函數,只有在沒有找到屬性的情況下,才調用__getattr__,已有的屬性不會在__getattr__中查找。
當我們實現了__getattr__方法后,調用任何實例沒有的屬性都會返回None,這是因為我們定義的__getattr__()方法默認返回None,要讓類只響應幾個特點的屬性,對於其他的屬性,我們就要按照約定,拋出AttributeError的錯誤:
>>> class Student(object): def __getattr__(self, attr): if attr == 'name': return 'Alice' raise AttributeError('\'Student\' object has no attribute \'%s\'' % attr) >>> s = Student() >>> s.age Traceback (most recent call last): File "<pyshell#49>", line 1, in <module> s.age File "<pyshell#47>", line 6, in __getattr__ raise AttributeError('\'Student\' object has no attribute \'%s\'' % attr) AttributeError: 'Student' object has no attribute 'age
這實際上可以把一個類的所有屬性和方法調用全部動態化處理,不需要其他的特殊手段。這種完全動態調用的特性有什么實際的作用呢?作用就是可以針對完全動態的情況來調用。如要寫SDK,如果給每個URL對應的API都寫一個方法,那太困難了,而且API一旦改變SDK也要改。所以此時可以利用完全動態的__getattr__來寫一個鏈式調用:
class Chain(object): def __init__(self, path=''): self._path = path def __getattr__(self, path): return Chain('%s/%s' % (self._path, path)) def __str__(self): return self._path __repr__ = __str__
試試:
>>> Chain().status.user.timeline.list '/status/user/timeline/list'
__call__
我們知道一個對象額實例可以有自己的屬性和方法,我們可以用instance.method()來調用,那么能不能直接在實例本身上調用呢?OK,只需要實現__call__()方法。請看示例:
>>> class Student(object): def __init__(self, name): self.name = name def __call__(self): print('My name is %s' % self.name) >>> s = Student('Alice') >>> s() My name is Alice
__call__()還可以定義參數相當於對一個函數進行調用,所以你完全可以把對象看成函數,函數看成對象,這2者本來就沒有根本的區別。如果你把對象看成函數,那么函數本身也可以在運行期動態地創建出來(類的實例都是運行期創建出來的)。
Tips:通過callable()函數,可以判斷一個對象是否是“可調用”對象。
枚舉類
我們應該或多或少都知道一點關於枚舉的知識,枚舉就是列舉一個可數集合的元素。如,人的性別可以看成一個集合,通過枚舉,可以拿到‘男’和‘女’。在Python中,提供了Enum類來實現枚舉這個功能:
from enum import Enum Sex = Enum('Sex', ('Male', 'Female'))
我們可以直接使用Sex.Male來引用一個常量:
>>> Sex.Male
<Sex.Male: 1>
也可以枚舉出它的所有成員:
>>> for name, member in Sex.__members__.items(): print(name, '=>', member, ',', member.value) Male => Sex.Male , 1 Female => Sex.Female , 2
注意:value屬性是自動賦給成員的int常量,默認從1開始計數。
我們也可以從Enum派生出自定義類,用於更精確地控制:
from enum import Enum, unique @unique #@unique裝飾器可以幫我們檢查保證有沒有重復值 class Sex(Enum): Male = 0 #Male的value被設定為0 FeMale = 1
for name, member in Sex.__members__.items(): print(name, '=>', member, ',', member.value) ''' Male => Sex.Male , 0 FeMale => Sex.FeMale , 1 '''
訪問這些枚舉類型可以有若干種方法:
>>> male = Sex.Male >>> print(male) Sex.Male >>> male = Sex['Male'] >>> print(male) Sex.Male >>> male = Sex(0) >>> print(male) Sex.Male >>>
Tips:枚舉常量既可以使用成員名稱引用,又可以直接根據value的值獲取。
元類
type()
我們應該知道動態語言和靜態語言最大的不同,就是函數和類的定義不是在編譯時定義的,而是在運行是動態創建的。比如說我們要定義一個Hello的類,就寫一個hello.py的模塊:
class Hello(object): def hello(self, name='world'): print('Hello,%s' % name)
當Python解釋器載入hello模塊時,就會依次執行該模塊的所有語句,執行結果就是動態創建出一個Hello的class對象,測試如下:
>>> from hello import Hello >>> h = Hello() >>> h.hello() Hello,world
type()函數可以查看一個類型或變量的類型,Hello是一個class,它的類型就是type,而h是一個實例,它的類型是class Hello:
>>> print(type(Hello)) <class 'type'> >>> print(type(h)) <class 'hello.Hello'>
我們已經說過了class的定義是運行時動態創建的,而創建class的方法就是使用type()函數。那么type()函數就既可以返回一個對象的類型,又可以創建出新的類型。所以我們就應該可以通過type()函數創建出hello類,而不需通過class Hello(object)的定義。試試:
>>> def fn(self, name='world'): #先定義函數 print('Hello, %s.' % name) >>> Hello = type('Hello', (object,), dict(hello=fn)) #創建出Hello class >>> h = Hello() >>> h.hello() Hello, world. >>> print(type(Hello)) <class 'type'> >>> print(type(h)) <class '__main__.Hello'>
要創建一個class對象,type()函數依次傳入3個參數:1.class的名稱 2.繼承的父類集合,如果只有一個父類,別忘了tuple的單元素寫法 。 3.class的方法的名稱與已定義的函數綁定(這里,我們把函數fn綁定到hello上)。
Tips:通過type函數創建出來的類和直接寫class是一樣的,本質上都是通過type()函數創建出class。動態語言本身支持運行期動態創建類,而如果要在靜態語言運行期間創建類,必須構造源代碼字符創再調用編譯器,或者借助一下工具生成字節碼實現,會非常復雜,但本質都是動態編譯。
metaclass(元類)
前面已經說過了type()可以動態地創建類,但除此之外,還可以使用metaclass以控制類的創建行為。什么是metaclass呢?簡單的解釋就是:類是metaclass創建出來的“實例”,metaclass是類的“模板”。使用metaclass時,就先定義metaclass,再創建類,最后創建出實例。
那metaclass到底有什么用呢?不急,我們先來看一個簡單的例子,先定義出一個簡單的metaclass(用來干啥?不急,先定義出來。),定義ListMetaclass(元類默認以‘Metaclass’結尾):
#metaclass是類的模板,所以必須從‘type’類型派生 class ListMetaclass(type): def __new__(cls, name, bases, attrs): attrs['add'] = lambda self, value: self.append(value) return type.__new__(cls, name , bases, attrs)
有了這個元類,我們再定義一個普通的類,指示使用元類來定制類(傳入關鍵字參數metaclass):
class MyList(list, metaclass=ListMetaclass): pass
這樣,magic就生效了,它指示Python解釋器在創建MyList時,要通過ListMetaclass.__new__()來創建。這樣,就可以定制MyList類了,比如加上新的方法add。來說下__new__()方法,該方法一共接收4個參數,分別是:1.當前准備創建類的對象 2.類的名字 3.類繼承的父類集合 4.類的方法集合。
此時,應該明白ListMetaclass代碼的含義了:將需定制類的add方法(沒有就創建)修改為“為實例對象添加值”,並返回給MyList類。來測試下:
>>> L = MyList() >>> L.add(1) >>> L [1]
好想是很magic,但為什么要這樣呢?動態修改有什么意義呢?直接在類定義上add()不是更簡單嗎?正常情況下,確實如此,但是總會遇到需要通過metaclass修改類的定義的,如ORM。
那問題又來了,什么是ORM?學過數據庫或者用過數據庫的應該知道,ORM全稱“Object Relation Mapping”,即對象-關系映射。簡單的來說,就是把關系數據庫的一行映射成為一個對象,一個類對應成一張表。
所以為了說明metaclass的強大之處,讓我們來嘗試編寫一個ORM框架吧。
1.編寫底層模塊的第一步,就是先把調用接口寫出來。比如,使用者如果使用這個ORM框架,想定義一個User類來操作對應數據表User,使用者應該寫出這樣的代碼來調用:
class User(Model): #定義類的屬性到列的映射 id = IntegerField('id') name = StringField('username') email = StringField('email') password = StringField('password') # 創建一個實例: u = User(id=123, name ='Alice', email='xxx@orm.org', password='xxxx') #保存到數據庫 u.save()
其中,父類Model和屬性類型StringField、IntegerField由ORM框架提供,剩下的魔術方法如save()全部由metaclas自動完成。這樣metaclass的編寫雖然會比較復雜,但ORM的使用者調用起來卻十分簡單。
2.現在就按照上面的接口定義,來實現該ORM,我們先定義一個最基本的Field類用於保存數據庫表中的字段名和字段類型:
class Field(object): def __init__(self, name, column_type): self.name = name self.column_type = column_type def __str__(self): return '<%s:%s>' % (self.__class__.__name__, self.name)
3.有了最基本的Field定義,我們就可以擴展定義各種類型的Field了,如StringField、IntegerField等。
class StringField(Field): def __init__(self, name): super(StringField, self).__init__(name,'varchar(100)') #調用父類的__init__方法 class IntegerField(Field): def __init__(self, name): super(IntegerField, self).__init__(name, 'bigint') #調用父類的__init__方法
4.編寫ModelMetac元類用於定制基類Model及基類:

class ModelMetaclass(type): def __new__(cls, name, bases, attrs): if name =='Model': #不對Model類進行修改 return type.__new__(cls, name, bases, attrs) print('Found model: %s' % name) mappings = dict() for k, v in attrs.items(): #在當前類查找出定義的類的所有屬性,保存到mappings中 if isinstance(v, Field): print('Found mapping: %s ==> %s' %(k, v)) mappings[k] = v for k in mappings.keys(): #從類的屬性中刪除該Field屬性,否則容易造成運行錯誤(實例的屬性會遮住類的同名屬性) attrs.pop(k) attrs['__mappings__'] = mappings #保存屬性和列的映射關系 attrs['__table__'] = name #將表名和類名設置成一樣的 return type.__new__(cls, name, bases, attrs) class Model(dict, metaclass=ModelMetaclass): def __init__(self, **kw): super(Model, self).__init__(**kw) def __getattr__(self, key): try: return self[key] except KeyError: raise AttributeError(r"'Model' object has no attribute '%s'" % key) def __setattr__(self, key, value): self[key] = value def save(self): fields = [] params = [] args = [] for k, v in self.__mappings__.items(): fields.append(v.name) params.append('?') args.append(getattr(self, k, None)) sql = 'insert into %s (%s) values (%s)' % (self.__table__, ','.join(fields), ','.join(params)) print('SQL: %s' % sql) print('ARGS: %s' % str(args))
5.ok,開始調用:
# 創建一個實例: u = User(id=12345, name='Michael', email='test@orm.org', password='my-pwd') #保存到數據庫 u.save() ''' Found model: User Found mapping: id ==> <IntegerField:id> Found mapping: name ==> <StringField:username> Found mapping: email ==> <StringField:email> Found mapping: password ==> <StringField:password> SQL: insert into User (id,username,email,password) values (?,?,?,?) ARGS: [12345, 'Michael', 'test@orm.org', 'my-pwd'] '''
成功了,好像還可以啊(溜了~~~)。
Tips:metaclass是Python非常具有魔術性的對象,他可以改變類創建時的行為,這么牛逼的功能使用起來還是要小心點的。