廖雪峯Python教程閱讀筆記——7. 面向對象高級編程

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中非常具有魔術性的對象,它可以改變類創建時的行爲,這種強大的功能使用起來務必小心。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章