7 面向對象高級編程
數據封裝、繼承和多態只是面向對象程序設計中最基礎的3個概念,還有其他的高級特性,比如:多重繼承、定製類、元類等概念
7.1 使用slots
正常情況下,當我們定義了一個class,創建了一個class實例後,我們可以給該實例綁定任何屬性和方法,這就是動態語言的靈活性,比如:
class Student(object):
pass
然後,嘗試給實例綁定一個屬性:
>>>s = Student()
>>>s.name = 'Mical'
>>>print(s.name)
Mical
還可以嘗試給實例綁定一個方法:
>>>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
但是,給一個實例綁定方法,對另外一個實例並不起作用。爲了解決,可以給class類直接綁定方法:
>>>def set_score(self,score):
... self.score = score
...
>>>Student.set_score = set_score #給類綁定一個方法
這樣,該類Student 就可以使用該方法。通常情況下,上面的set_score()
方法可以定義在類中。
但是,如果我們想要限制實例的屬性,比如只能對Student
類添加name和age屬性。我們可以使用在定義class時,定義一個特殊的__slots__
變量,來限制class實例能添加的屬性:
class Student(object):
__solts__ = ('name','age') #用tuple定義允許綁定的屬性名稱
注意,__slots__
定義的屬性只針對當前類起作用,對繼承該類的子類不起作用。
7.2使用@property
@property
是一個裝飾器,負責把一個方法變成屬性調用:
class Student(object):
@property #把一個getter變成屬性,只需要加上@property
def score(self):
return self._score
@score.setter #@property又創建了一個@score.setter 把setter方法編程屬性
def score(self,value):
if not isinstance(value,int):
raise ValueError('score must be an integer')
if value < 0 and value > 100:
raise ValueError('score must be between 0~100')
self._score = value
@property的實現比較複雜,我們在對實例操作的時候,就知道該屬性很肯能不是直接暴露的,而是通過getter和setter方法來實現的。
小結:
@property廣泛應用在類的定義中,可以讓調用者寫出間斷的代碼,同時保證對參數進行必要的檢查。
7.3 多重繼承
對於子類C,既可以繼承父類A,又可以繼承父類B:
class C(A,B):
pass
通過多重繼承關係,一個子類可以獲得多個父類的多個方法。
- MixIn
多重繼承關係,比如上述子類C,既可以繼承父類A,又可以繼承父類B,這種行爲成爲MixIn
無需類似於java如果要繼承,有強大的繼承鏈。Python只需要選擇組合不同的類功能,就可以快速構造出所需的子類。
小結:
Python允許使用多重繼承,因此,MixIn是一種常見的涉及。
只允許單一繼承的語言-Java,不能使用MixIn涉及
7.4 定製類
看到類似__slots__
這種形如__XXX__
的變量或者函數時,這在Python中是有特殊用途的。
- str
class Student(object):
def __init__(self,name):
self.name = name
我們打印一個實例:
>>>print(Student("Mical"))
<__main__.Student object at 0x109afb190>
我們看到,打印的是一個對象的地址信息,如何能打印我們能看懂的字符串呢?
只需要定義一個__str__()
方法,返回一個好看的字符串就行了:
class Student(object):
def __init__(self,name):
self.name = name
def __str__(self):
return 'Student object (name :%s) % self.name'
下面看兩種調用方式:
1.打印:
>>>print(Student('Hello'))
Student object (name :Hello)
>>>s = Student('Hello')
>>>s
<__main__.Student object at 0x109afb190>
注意到兩者顯示的效果不一樣,這是因爲直接顯示變量s,並不是調用的__str()__
而是調用的__repr()__
,兩者的區別是__str()__
返回的是用戶看到的字符串,__repr()__
返回的是程序開發者看到的,用於程序調試
__iter__
如果一個類想被用於for…in循環,類似list或tuple那樣,就必須實現一個__iter__()
方法,該方法返回一個對象,然後Python的for循環就會不斷的調用該迭代對象的__next__()
方法,拿到下一個元素的值,直到遇到StopIteration
異常纔會停止。
我們編寫一個類,使其可以for循環,比如斐波那契數列函數:
class Fib(object):
def __init__(self):
self.a,self.b = 0,1 #初始化兩個迭代對象
def __iter__(self):
return self #實例本身就是迭代對象
def __next__(self):
self.a,self.b = self.b,self.a+self.b #計算下一個值
if self.a>100000: #退出循環
raise StopIteration
return self.a #返回下一個值
現在,試圖遍歷Fib()類的實例
>>>for n in Fib():
... print(n)
...
1
1
2
3
5
- getitem
Fib實例雖然能作用於for循環,但是,把它當list來使用,還是不行,比如獲取第index個元素:
>>>Fib()[5]
會提示Fib對象並不支持indexing
對Fib類進行改造:
class Fib(object):
def __getitem__(self, n):
a, b = 1, 0
for x in range(n):
a, b = a,a+ b
return a
現在就可以使用下標來訪問Fib()實例對象了
7.5 使用枚舉類
Python提供了Enum
類來實現每個變量都是class
的一個實例:
>>>from enum import Enum
>>>Month = Enum('Month',('Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'))
這樣,我們獲取一個Month類的枚舉類型。可以使用Month.Jan來引用一個常量,或者枚舉他的所有成員。
for name,member in Month.__members__.items():
print(name,'=>',member,',',member.value)
value屬性則是自動賦值給成員int常量,默認從1開始。
如果需要更精確的控制枚舉類型,可以從Enum派生出自定義的類:
from enum import Enum,unique
@unique
Class Weekday(Enum):
Sun=0 #Sun的value被設定爲0
Mon=1
Tue=2
Wed=3
Thu=4
Fri=5
Sat=6
@unique裝飾器可以檢查枚舉類型中有無重複元素
訪問這些枚舉類型可以有一下幾種方式:
>>>Weekday.Sun
>>>Weekday['Sun']
>>>Weekday(1)
>>>Weekday.Sun.value
7.6使用元類
- type()
動態語言和靜態語言不同,就是函數和類的定義,不是編譯時定義的,而是在運行時動態創建的。
比如定義一個Class Hello()類,當Python載入Hello模塊時,就會依次執行該模塊中的所有語句,執行結果就是動態創建出一個Hello的class對象。
>>>hello = Hello()
>>>print(type(Hello))
<class 'type'>
>>>print(type(hello))
<class hello.Hello>
type()函數可以查看一個類型或變量的類型,Hello是一個class,它的類型就是type,而hello是一個實例,它的類型就是class Hello
type()函數不僅可以查看類型,也可以創建一個類:
>>>#先定義函數
>>>def fn(self,name='world'):
print('hello, %s.' % name )
>>>Hello = type('Hello',(object,),dict(hello=fn))#創建Hello class
要創建一個class對象,type()
函數一次傳入三個參數:
1. class的名稱
2. 繼承的父類集合,注意Python支持多重繼承,如果只有一個父類,別忘了tuple單元素的寫法:用”,”號
3. class方法名稱與函數綁定,這裏我們把函數fn綁定到方法名hello上
通過type()函數創建的類和直接寫class是完全一樣的。
- metaclass
除了使用type()
動態創建類外,要控制類的創建行爲,還可以使用metaclass
metaclass
,稱爲元類。
當我們定義了類以後,就可以根據這個類創建出示例,所以順序爲:先定義類,再創建實例。如果我們要創建類,必須先定義metaclass
。
metaclass
允許創建類和修改類,也可以認爲,類是metaclass
創建出的“實例”
例如:給我們自定義的類MyList增加一個add方法:
定義ListMetaclass
,按照默認習慣,metaclass 的類名總是以Metaclass結尾,以便清楚地表示這是一個metaclass:
#metaclass 是類的模板,所以必須從'type'類型派生:
class ListMetaclass(type):
def __new__(cls,name,bases,attrs):
attrs['add'] = lambda self,value: self.append(value)
retrun type.__new__(cls,name,bases,attrs)
有了MetaClass類這個局部模板,我們可以定義類時,指示使用ListMetaclass
元類來定製這個類:
class MyList(list,metaclass=ListMetaclass):
pass
當我們傳入參數metaclass=ListMetaclass
時,它指示Python解釋器在創建 Mylist
時,要通過ListMetaclass._new__()
來創建,在此,我們可以修改類的定義,比如加上新方法,然後返回修改後的定義。
__new__()
方法接收的參數依次是:
1. 當前準備創建的類的對象 #cls
2. 類的名字 #name
3. 類繼承的父類集合 #bases
4. 類的方法集合 #attrs
通常情況下,如果要定義某一個方法,在類定義時,就已經確定,但是也有另外情況——ORM.
ORM稱爲”Object Relation Mapping”,對象——關係映射,要編寫一個ORM框架,所有的類都只能動態定義,因爲只有使用者才能根據表的結構定義出對應的類來。
舉例寫一個ORM框架:
1. 編寫底層模塊的第一步,就是先把調用接口寫出來,比如使用者使用這個ORM框架,想定義一個User類來操作對應的數據庫表user.我們期待他寫出的代碼是:
class User(Model):
#定義類的屬性到列的映射
id = IntegerField('id')
name = StringField('username')
email = StringField('email')
password = StringField('password')
創建一個實例
u = User(id=12345,name='Weishzhu',email='[email protected]',password='weishzhu')
#保存到數據庫
u.save()
其中,父類Model
和域類型IntegerField
|StringField
都是由ORM提供的。剩下的方法,比如save()
全部由metaclass自動完成。雖然metaclass的編寫會比較複雜,但orm的使用者用起來會非常簡單
現在我們按照上面的接口來實現該ORM
首先定義Filed類,它負責保存數據庫表字段名和字段類型:
class Field(object):
def __init__(self,name,column_type):
self.name = name
self.column_type
def __str__(self):
return '<%s,%s>' % self.__class__.name,self.name
在Field的基礎上,進一步定義各種類型的Field,比如IntegerField
|StringField
:
class IntegerField(Filed):
def __init__(self,name):
super(IntegerField,self).__init__(name,'bigint')
class StringField(Field):
def __init__(self,name):
super(StringField,self).__init__(name,'varchar(100)')
下面就是編寫複雜的ModelMetaclass類了:
class ModelMetaclass(type):
def __new__(cls,name,bases,attrs):
if name='Model'
return type.__new__(cls,name,bases,attrs)
print('Found model:%s' % name)
mappings = dict()
for k,v in attrs.items():
if isinstance(v,Field):
print('Found mapping :%s ==> %s' % (k,v)))
mapping[k] = v
for k in mappings.keys():
attrs.pop(k)
attrs['__mappings__'] = mappings #保存屬性和列的映射關係
attrs['__table__'] = name #假設表名和類名一致
return type.__new__(cls,name,bases,attrs)
#基類 Model
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' % attr(args))
當用戶定義一個class User(Model)時,Python解釋器首先在當前類User的定義中查找metaclass ,如果沒有找到,則在父類Model中找metaclass。找到了就用Model中定義的metaclass的ModelMetaclass來創建一個User
類,也就是說metaclass可以隱式地繼承到子類。但子類自己感覺不到。
在ModelMetaclass中,要做以下幾步:
1. 排除掉隊Model類的修改;
2. 在當前類中,比如User中查找定義的類的所有屬性,如果找到一個Filed屬性,就把它保存到一個mappings
的dict中,同時從類屬性中刪除該Field屬性,否則,容易造成運行時錯誤。
3. 把表名保存到__table__
中,這裏簡化爲表名默認爲類名。
在Model類中,就可以定義各種操作數據庫的方法,save()
、update()
小結:
metaclss是Python中非常具有魔術性的對象,它可以改變類創建時的行爲,這種強大的功能使用起來務必小心。