Python語法速查: 8. 類與對象

返回目錄html

 

本篇索引python

(1)類基本用法編程

(2)類的進階知識app

(3)類繼承框架

(4)propertyssh

(5)描述符函數

(6)__slots__測試

(7)元類ui

 

 

 (1)類基本用法

使用面向對象編程有3個重要的好處:封裝(Encapsulation)、繼承(Inheritance)、多態(Polymorphism)。 Python中全部的類都是繼承自object類(Python2中要使用__metaclass__=type聲明,Python3中不用考慮),它提供了一些經常使用的方法(如__str__()等)的默認實現,在使用中能夠不顯式指出object。spa

所以如下三種定義類的方法都是能夠的:

class MyClass(object):
    pass
    
class MyClass():
    pass
    
class MyClass:
    pass

 

● 類中的成員

供全部實例共享的變量稱爲「類變量」(class variables)。類的每一個實例各自有的成員變量稱爲「特性」(attribute);而「屬性」(property)有另外的含義,後面會詳述。 因爲之前Java中「屬性」這個名詞太深刻人心,所以在不引發歧義的狀況下,也經常把「特性」叫爲屬性。

類中定義的函數分爲三種:實例方法(綁定方法)、類方法、靜態方法。

如下爲類中各類成員的定義:

class Foo:
    a = 10;   # 類變量(供全部實例共享)
    
    # 函數fi爲實例方法
    def fi(self):
        self.b = 20   # 特性(每一個實例各自有)
        self.a = 30   # 本句不會改變類變量a,而是爲本實例建立新的實例attribute:a
        
    # 函數fc爲類方法(使用@classmethod裝飾器)
    @clasmethod
    def fc(cls):
        cls.a = 30
        
    # 函數fs爲靜態方法(使用@staticmethod裝飾器)
    def fs():
        print('hello')
        
f = Foo()
f.fi()      # 調用實例方法(綁定方法)
Foo.fc()    # 調用類方法
Foo.fs()    # 調用靜態方法

 

● 初始化類實例

在建立一個類的實例時,會自動調用類的__init__()方法,這個有點像C++或Java中的構造函數,能夠在__init__()方法對實例進行一些初始化工做。 __init__()方法的第一個入參默認爲self,後面的入參可任意添加。

因爲在Python中使用變量前無需聲明,所以一個比較好的習慣是在__init__()方法中,對全部要用到的「特性」進行初始化。

初始化實例的例子:

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

 

● 綁定與非綁定方法

經過實例調用方法時,有綁定和非綁定兩種用法。綁定方法封裝了成員函數和一個對應實例,調用綁定方法時,實例會做爲第一個參數self自動傳遞給方法。而非綁定方法僅封裝了成員函數,並無實例,用戶在調用非綁定方法時,須要顯式地將實例做爲第一個參數傳遞進去。詳見下面2個例子

綁定用法(bound method):

class Foo():
    def meth(self, a):
        print(a)

obj = Foo()     # 建立一個實例
m = obj.meth    # 將meth方法綁定到obj這個實例上
m(2)            # 調用這個方法時,Python會自動將obj做爲self參數傳遞給meth()方法

非綁定用法(unbound method):

class Foo():
    def meth(self, a):
        print(a)

obj = Foo()     # 建立一個實例
um = Foo.meth   # 非綁定,僅僅是將這個方法賦值給um,並不須要實例存在
um(obj, 2)      # 調用這個非綁定方法時,用戶須要顯式地傳入obj實例做爲第一個參數。

 

● 類方法、靜態方法

類方法用@classmethod裝飾器來修飾定義,靜態方法用@staticmethod裝飾器來修飾定義,它們的區別僅僅在於: 類方法能夠訪問類變量,而靜態方法不能放問類變量。因爲類方法須要訪問類變量,所以它的第一個參數默認爲cls(類名)。

能夠經過類名來調用類方法和靜態方法,也能夠經過實例名來調用類方法和靜態方法。其使用方法見前例。

 

● 數據封裝和私有性

默認狀況下,類的全部屬性和方法都是「公共的」,能夠在外部被訪問,也會被派生類繼承。爲了實現使屬性私有,Python提供了點小技巧: 能夠將某個屬性名或方法名前加雙下劃線(__),這樣Python會對這個名稱自動變形,變成:「_類名__成員名」的形式, 這樣外部就不能簡單經過「成員名」訪問內部屬性和內部方法了。固然,這只是個小trick,外界經過實例的__dict__屬性, 仍是能夠查看到這個變形後的真實名稱的。

class Foo:
    def __init__(self):
        self.__x = 3    # 變形爲 self._Foo__x
        
    def __bar(self):    # 變形爲 self._Foo__bar()
        pass

 

● 實例的__dict__、__class__屬性

每一個實例內部能夠看做都是用字典來實現的,能夠經過實例的__dict__屬性訪問這個字典,其中包含了每一個屬性的「鍵值對」。 對實例的修改始終都會反映到局部__dict__屬性中。一樣,若是直接對__dict__進行修改,所做的修改也會反映在實例的屬性中。

實例的__class__屬性保存了這個實例所屬的類。

class Foo:
    a = 10
    def __init__(self):
        pass
        
    def bar(self):
        self.b = 20
        
f = Foo()
print(f.__dict__)   # 本句結果爲:{}
f.bar()
print(f.__dict__)   # 本句運行時,因爲bar()方法已運行,故爲:{'b':20}

f.__dict__['b'] = 30
print(f.b)          # 本句運行結果爲:30

print(f.__class__)  # 本句結果爲:<class '__main__.Foo'>

 

● 類的__dict__、__bases__屬性

類的__dict__屬性反映的是和實例的__dict__徹底不一樣的內容,本質上是類對象的屬性列表(Python中萬物皆對象,類定義自己也是個對象,是type類型的一個實例)。

類的__bases__屬性爲一個元組,按順序列出了這個類的繼承鏈。

class Foo:
    a = 10
    def __init__(self):
        pass
        
    def bar(self):
        self.b = 20
    
print(Foo.__dict__)  
print(Foo.__bases__)

# 結果爲:
{'__module__': '__main__', 'a': 10, '__init__': <function Foo.__init__ at 0x000002A3FFEDB1F8>, 'bar': <function Foo.bar at 0x000002A3FFEDB288>, '__dict__': <attribute '__dict__' of 'Foo' objects>, '__weakref__': <attribute '__weakref__' of 'Foo' objects>, '__doc__': None}
(<class 'object'>,)

 

● 使用dir()內置函數

使用dir()內置函數能夠獲得實例內的全部:屬性名、方法名。用戶能夠經過自定義__dir__()方法來使dir()只顯示本身想要顯示給外界的名稱。

class Foo:
    a = 10
    def bar(self):
        self.b = 20
        
f = Foo()
print(dir(f))

# 結果爲:
['__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__', 'a', 'bar']

 

 

 

 (2)類的進階知識

● 類對象

Python中萬物皆對象,類定義自己也是一個對象,這個「類定義」對象的類型爲type。所以,爲了表示區別, 由這個類定義生成的對象通常稱爲「實例對象」(instance),或簡稱「實例」,再也不稱爲對象(object)。 固然,因爲之前「對象」這個名詞太深刻人心,因此有時也在不引發歧義的狀況下,把「實例」叫爲「對象」

類對象和實例對象的區別:

class Foo:
    pass
f = Foo()
    
type(Foo)   # 結果爲:<class 'type'>
type(f)     # 結果爲:<class '__main__.Foo'>

t = Foo     # 將類對象(即type類型的對象)賦值給t

 

type對象的經常使用屬性:

屬性 描述
t.__doc__ 文檔字符串
t.__name__ 類名稱
t.__bases__ 由基類構成的元組
t.__mro__ 在實例方法查找時的各基類順序元組(method resolution)
t.__dict__ 保存類方法和變量的字典
t.__module__ 定義類的模塊名稱
t.__abstractmethods__ 抽象方法名稱的集合

 

實例對象的經常使用屬性:

屬性 描述
i.__class__ 實例所屬的類
i.__dict__ 保存實例數據的字典

 

 

● 對象的特殊方法

對象的特殊方法名稱先後都帶有雙下劃線,當程序執行時,這些方法由解釋器自動觸發。例如:x + y 被映射爲內部方法: x.__add__(y)、索引操做 x[k] 被映射爲:x.__getitem__(k),下面介紹一些常見的特殊方法:

 

a. 對象的建立與銷燬

方法 描述
__new__(cls [,*args [,**kwargs]]) 建立新實例時調用的類方法
__init__(self [,*args [,**kwargs]]) 初始化新實例時調用
__del__(self) 銷燬實例時調用

用戶通常不多須要本身定義__new__()方法和__del__()方法,__new__()方法僅在定義元類或繼承自不可變類型時才使用。 __del__()基本不會用到,若是要釋放某種資源,更好的方法是自定義一個close()方法,而後顯式調用來釋放資源。

實例建立的內部過程:

f = Foo.__new__(Foo)
if isinstance(f, Foo):
    Foo.__init__(f)

若是用戶自定義了__new__(),一般這個類可能繼承自不可變的內置類型(如字符串),由於__new__()是惟一在建立實例以前執行的方法, 若是在__init__()中修改,可能爲時已晚,見下例:

class Upperstr(str):
    def __new__(cls, value=''):
        return str.__new__(cls, value.upper())
        
u = Upperstr('hello');    # 能夠在__new__()中將字符串變爲大寫:HELLO

 

b. 對象的字符串表示

方法 描述
__format__(self, format_spec) 建立格式化後的表示
__repr__(self) 建立對象的完整字符串表示
__str__(self) 建立對象的簡單字符串表示

內建函數repr()會自動調用對象的__repr__()方法,內建函數str()會自動調用對象的__str__()方法。 區別在於__str__()方法返回的字符串更加簡明易懂,如該方法未定義,就調用__repr__()方法。

__format__()方法的調用者是內建函數format()或字符串的format()方法,如:format(x, 'spec') 即爲調用:x.__format__('spec')

 

c. 對象的比較與排序

方法 描述
__bool__(self) 由內建函數bool()調用,測試對象真值
__hash__(self) 由內建函數hash()調用,計算對象的散列值
__lt__(self, other) self < other
__le__(self, other) self <= other
__gt__(self, other) self > other
__ge__(self, other) self >= other
__eq__(self, other) self == other
__ne__(self, other) self != other

通常來講,用戶無需實現全部上面的比較操做,但若要使用對象做爲字典鍵,則必須定義__eq__()方法, 若要爲對象排序或使用諸如 min() 或 max() 之類的內建函數,則至少要定義__lt__()方法。

 

d. 類型檢查

方法 描述
__instancecheck__(cls, object) 由內建函數 isinstance()調用
__subclasscheck__(cls, sub) 由內建函數 issubclass()調用

這個功能一般用於繞過喜歡使用類型檢查的用戶。在下例中,雖然FooProxy和Foo擁有一樣的接口,而且對Foo的功能進行了加強,但因爲FooProxy不是繼承自Foo, 所以通不過喜歡使用類型檢查的用戶:

class Foo(object):
    def bar(self, a, b):
        pass
        
class FooProxy(object):
    def bar(self, a, b):
        pirnt(a,b)
        
f = Foo()
g = Foo()
isinstance(g, Foo)    # 結果爲:False

下例中,能夠經過定義註冊方法register(),並從新定義__instancecheck__()和__subclasscheck__(),繞過默認的死板的類型檢查, 而實現更靈活的註冊制的類型檢查。

class IClass(object):
    def __init__(self):
        self.implementors = set()
    
    def register(self, C):
        self.mplementors.add(C)
    
    def __instancecheck__(self, x):
        return self.__subclasscheck__(type(x))
        
    def __subclasscheck__(self, sub):
        return any(c in self.implementors for c in sub.mro())

IFoo = IClass()
IFoo.register(Foo)
IFoo.register(FooProxy)

f = Foo()
g = FooProxy()
isinstance(f, IFoo)   # True
isinstance(g, IFoo)   # True

issubclass(FooProxy, IFoo)    # True  

上面代碼只是演示如何從新靈活定義__instancecheck__()方法的示例。一般若是要使用register()方法的話,抽象基類是更好的選擇。

 

e. 屬性(attribute)訪問

方法 描述
__getattribute__(self, name) 返回屬性 self.name
__getattr__(self, name) 若是上面的__getattribute__()未找到屬性,則自動調用本方法
__setattr__(self, name, value) 設置屬性 self.name = value
__delattr__(self, name) 刪除屬性 self.name

只要使用了 obj.name = val 語句設置了屬性,實例的__setattr__('name', val)方法就會自動被調用。

一樣的,只要使用了 del obj.name 語句,實例的__delattr__('name')方法就會自動被調用。

而在讀取屬性內容時,稍微複雜一點,例如讀取 obj.name 時:實例的__getattribute__('name')方法會被自動調用, 該方法執行搜索來查找該屬性,這一般涉及檢查特性、查找局部__dict__屬性、搜索基類等等。若是搜索過程失敗, 最終會嘗試調用實例的__getattr__()方法(若是已定義)來查找該屬性。若是這也失敗,就會引起AttributeError異常。

用戶經過從新定義__getattr__()、__setattr__()、__delattr__()方法,能夠截取屬性操做,並將自定義的操做添加進去。

下例定義了2個虛擬屬性:area, perimeter

class Circle(object):
    def __init__(self, r):
        self.r = r
        
    def __getattr__(self, name):
        if name == 'arer':
            return 3.14 * self.r ** 2
        elif name == 'perimeter':
            return 2 * 3.14 * self.r
        else:
            return object.__getattr__(self, name)
            
    def __setattr__(self, name, value):
        if name in ('area', 'perimeter'):
            raise TypeError('%s is readonly' %name)
        object.__setattr__(self, name, value)

 

f. 屬性(property)包裝與描述符

方法 描述
__get__(self, instance, cls) 返回一個屬性值,不然引起AttributeError異常
__set__(self, instance, value) 將屬性設爲value
__del__(self, instance) 刪除屬性

描述符使用,詳見後文「描述符」小節詳細說明。

 

g. 序列與映射方法

方法 描述
__len__(self) 返回self的長度,由內建函數 len() 調用
__getitem__(self, key) 返回 self[key],key甚至能夠是slice對象
__setitem__(self, key, value) 設置 self[key] = value
__delitem__(self, key) 刪除 self[key]
__contains__(self, obj) 若是obj在self中則返回True(主要由 in 操做符使用)

 

h. 數學操做方法

方法 描述
__add__(self, other) self + other
__sub__(self, other) self - other
__mul__(self, other) self * other
__truediv__(self, other) self / other
__floordiv__(self, other) self // other
__mod__(self, other) self % other
__divmod__(self, other) divmod(self, other)
__pow__(self, other [,modulo]) self ** other, pow(self, other, modulo)
__lshift__(self, other) self << other
__rshift__(self, other) self >> other
__and__(self, other) self & other
__or__(self, other) self | other
__xor__(self, other) self ^ other
__radd__(self, other) other + self
__rsub__(self, other) other - self
__rmul__(self, other) other * self
__rtruediv__(self, other) other / self
__rfloordiv__(self, other) other // self
__rmod__(self, other) other % self
__rdivmod__(self, other) divmod(other, self)
__rpow__(self, other [,modulo]) other ** self
__rlshift__(self, other) other << self
__rrshift__(self, other) other >> self
__rand__(self, other) self & other
__ror__(self, other) self | other
__rxor__(self, other) other ^ self
__iadd__(self, other) self += other
__isub__(self, other) self -= other
__imul__(self, other) self *= other
__itruediv__(self, other) self /= other
__ifloordiv__(self, other) self //= other
__imod__(self, other) self %= other
__ipow__(self, other [,modulo]) self **= other
__ilshift__(self, other) self <<= other
__irshift__(self, other) self >>= other
__iand__(self, other) self &= other
__ior__(self, other) self |= other
__ixor__(self, other) self ^= other
__neg__(self, other) -self
__pos__(self, other) +self
__abs__(self, other) abs(self)
__invert__(self, other) ~self
__int__(self, other) int(self)
__long__(self, other) long(self)
__float__(self, other) float(self)
__complex__(self, other) complex(self)

經過向類添加以上方法,可使實例可以使用標準數學運算符進行運算。下例演示瞭如何定義一個複數類,並讓其實例支持加減操做。 從下例中能夠看出,__add__()和__sub__()僅適用於Cmpx實例出如今運算符左側的情形。

class Cmpx(object):
    def __init__(self, real, imag = 0):
        self.real = float(real)
        self.imag = float(imag)
        
    def __repr__(self):
        return "Complex(%s,%s)" %(self.real, self.imag)
        
    def __str__(self): 
        return "(%g+%gj)" %(self.real, self.imag)
        
    # 重載加法操做符
    def __add__(self, other):
        return Cmpx(self.real + other.real, self.imag + other.imag)
        
    # 重載減法操做符
    def __sub__(self, other):
        return Cmpx(self.real - other.real, self.imag - other.imag)
        
c = Cmpx(1,2)
d = Cmpx(3,4)

print(c+d)      # 結果爲:(4+6j)
print(c+3.0)    # 結果爲:(4+2j)
print(c+'a')    # 報錯
print(3.0+c)    # 報錯

以上例子中,用到了other的real和imag屬性,若是other對象沒有這些屬性,那將會報錯。另外,倒過來操做(例如3.0+c)也會報錯, 由於內置的浮點類型的__add__()方法不知道關於Cmpx類的任何信息。若是須要這樣操做,須要向Cmpx類添加:逆向操做數(reversed-operand)方法。

class Cmpx(object):
    ......
    def __radd__(self, other):
        return Cmpx(self.real + other.real, self.imag + other.imag)
    ......

若是操做3.0+c失敗,Python將在引起TypeError異常前,先嚐試c.__radd__(3.0)

 

i. dir()與對象檢查

方法 描述
__dir__(self) 由內置函數dir()調用,返回對象內全部的名稱列表(屬性名、方法名等)

用戶定義該方法後,能夠隱藏一些不想讓其餘人訪問的對象內部細節。可是,其餘人仍能夠經過查看底層的__dict__屬性,瞭解類內部的細節。

 

 

 (3)類繼承

經過繼承建立新類時,新類將繼承其基類定義的屬性和方法,子類也能夠從新定義這些屬性和方法。在class語句中, 以逗號分隔的基類名稱列表來表示繼承,若是沒有指定基類,類將繼承object,object是全部Python對象的根類。

如下爲一個基本的類繼承例子:

class Shape:
    def get_name():
        return 'Shape'
    def area():
        return 0

class Square(Shape):
    def get_name():
        return 'square'
        
sq = Square()
sq.area()       # 自動調用基類的area()方法,返回0。

若是搜索一個屬性或方法時,未在實例中找到匹配的項,那麼會自動搜索其基類。

 

● 子類的初始化

子類定義__init__()方法時,不會自動調用基類的__init__()方法,所以,若是在子類的__init__()方法中須要調用基類的初始化方法, 須要顯式地寫出調用語句。有兩種方法能夠調用基類的__init__()方法:

class Shape:
    def __init__(self):
        self.name = 'Shape'
        self.area = 0
    def get_name():
        return self.name

# 方法一:
class Square(Shape):
    def __init__(self):
        Shape.__init__(self)
        self.name = 'Square'

        
# 方法二:
class Circle(Shape):
    def __init__(self):
        super().__init__()
        self.name = 'circle'

方法一中,須要顯式地指明基類的名稱,並將self做爲參數,去調用基類的__init__()方法。

方法二中,使用了super()內置函數,它會返回一個特殊的對象,該對象會自動去查找基類的屬性和方法,找到則執行這個方法。

 

● 多重繼承和mixin

雖然Python支持多重繼承,但最好避免使用多重繼承,由於它會使屬性的解析變得很是複雜。

只有一種狀況能夠很好地使用多重繼承,即便用mixin(混合類)。混合類一般定義了要「混合到」其餘類中的一組方法,目的是添加更多的功能。 一般mixin並不單獨做爲其餘類的基類使用,而是有點像插件,插入到其餘類中,爲其餘類添加某種單一的特定功能。

舉個例子:

class Shape:
    def __init__(self):
        self.name = 'Shape'
        self.area = 0
    def get_name():
        return self.name

class ColorMixin:
    color = 'white'
    def set_color(self,color):
        self.color = color
        
    def get_color(self):
        return self.color
        
class Square(Shape, ColorMixin):
    def __init__(self):
        Shape.__init__(self)
        self.name = 'Square'

class Circle(Shape, ColorMixin):
    def __init__(self):
        super().__init__()
        self.name = 'circle'


if __name__ == '__main__':
    s = Square()
    c = Circle()

    s.set_color('red')

    print(s.get_color())    # 輸出結果爲:red
    print(c.get_color())    # 輸出結果爲:white
    print(Circle.__mro__)   # 結果爲:(<class '__main__.Circle'>, <class '__main__.Shape'>, <class '__main__.ColorMixin'>, <class 'object'>)

在上例中,若是咱們要爲各類形狀的類增長「顏色」功能,就可以使用ColorMixin類,代碼比較簡單,很容易看懂。 使用mixin的前提是它提供的功能比較單一,不會和原有的繼承結構發生數據上的依賴和瓜葛。在Django等框架中,就大量使用了Mixin類。

 

● 抽象基類

要定義抽象基類須要用到abc模塊,該模塊定義了一個元類(ABCMeta)和一組裝飾器(@abstractmethod 和 @abstractproperty)。 抽象基類自己並不能直接實例化,必需要被子類繼承,並在子類上實現其抽象方法和抽象屬性後,才能在子類上實例化。

下例定義了一個抽象基類:

from abc import ABCMeta, abstractmethod, abstractproperty
class Foo(metaclass=ABCMeta):
    @abstractmethod
    def bar(self, a, b)
        pass
        
    @abstractproperty
    def name(self):
        pass

抽象基類支持對已經存在的類進行註冊(使用register()方法),使其屬於該基類,以下例所示:

class FooA(object)
    def bar(self, a, b):
        print('This is FooA method bar')
        
Foo.register(FooA)    # 向Foo抽象基類註冊

向抽象基類註冊某個類時,Python不會檢查該類是否實際實現了任何抽象方法或抽象屬性,這種註冊過程隻影響類型檢查(如isinstance()、issubclass()等內置函數)。

抽象基類主要用於幫助用戶創建自定義類的層次結構,一些Python模塊中,使用了抽象基類的做用, 如collections模塊中、numbers模塊中,都實現了層次結構的抽象基類。

 

 

 

 (4)property

property能夠定義一個虛擬的屬性,它的值並不保存在實際的內存中,而是每次讀取時,由臨時計算獲得。

下例中,area即爲虛擬屬性property:

import math
class Circle:
    def __init__(self, r):
        self.r = rad
        
    @property
    def area():
        return math.pi * self.r ** 2
        
c = Circle(1)
print(c.area)     # 結果爲:3.14

 

● property的設置和刪除

property不只能夠讀取,其實也能夠爲property定義設置函數和刪除函數。能夠用老式和新式兩種寫法, 老式的寫法更好理解一些,新式的寫法結構更清晰簡潔。

老式的寫法:

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

    def getname(self):
        return self.__name
        
    def setname(self):
        if not isinstance(value, str):
            raise TypeError('Must be a string')
        self.__name = value
          
    def delname(self):
        raise TypeError('Cannot delete name')
        
    name = property(getname, setname, delname)

上面的property函數,定義了虛擬屬性 name 的:讀取函數、設置函數、刪除函數。利用這種方法, 能夠將某個真實的成員變量(attribute)隱藏起來,而讓用戶只能經過property虛擬屬性來訪問某個成員變量。

 

新式寫法(使用裝飾器):

class Foo:
    def __init__(self, name):
        self.__name = name
    
    @property
    def name(self):
        return self.__name
    
    @name.setter
    def name(self, value):
        if not isinstance(value, str):
            raise TypeError('Must be a string')
        self.__name = value
        
    @name.deleter
    def name(self):
        raise TypeError('Cannot delete name')

 

 

 

 (5)描述符

描述符是一種建立託管屬性的方法。好比,一個類中有若干個屬性都要像上面那樣將真實的成員變量(attribute)隱藏起來, 每一個屬性都要定義:get、set、delete3個函數,寫起來太冗長。最好能一次定義好這3個函數,而後後面相似的屬性直接拿來用就能夠了, 這就是描述符的做用。

簡單來說,某個類只要內部定義了__get__()方法、__set__()方法、__delete__()方法中的一個或多個,這個類就是個「描述符」。先看一個例子:

下例中,TypedProperty就是個描述符

class TypedProperty:
    def __init__(self, name, type, default=None):
        self.name = "_" + name
        self.type = type
        self.default = default if default else type()
        
    def __get__(self, instance, cls):
        return getattr(instance, self.name, self.default)   # 這個getattr是Python內置函數
        
    def __set__(self, instance, value):
        if not isinstance(value, self.type):
            raise TypeError("Must be a %s" %self.type)
        setattr(instance, self.name, value)                 # 這個setattr是Python內置函數
      
    def __delete__(self, instance):
        raise AttributeError("Cannot delete attribute")
        
class Foo:
    name = TypedProperty("name", str)
    num  = TypedProperty("num", int, 42)
    
f = Foo()
g = Foo()

f.name = "abc"  # 調用Foo.name.__set__(f, 'abc')
g.name = "xyz"

a = f.name      # 調用Foo.name.__get__(f, Foo)
print(f.name, g.name)   #  本句運行結果爲:abc xyz
del f.name      # 調用Foo.name.__delete__(f)

在Foo類中,因爲name和num都須要相似的屬性操做,故都託管給TypedPropery描述符進行管理。須要注意的是, 描述符成員變量雖然是每一個實例各自有的(如上例中的name和num),但卻只能在類的級別進行定義(在Foo層面定義), 不能放在__init__()方法中初始化或在其餘方法中定義。

 

 

 

 (6)__slots__

經過定義特殊變量__slots__,類能夠限制用戶設置新的屬性名稱,以下例所示:

下例中對實例f設置新的屬性會引起AttributeError異常

class Foo:
    __slots__ = ('name', 'num')
    
f = Foo();
f.a = 2     # 本句會引起AttributeError異常

 

另外,因爲__slots__變量在實例內部再也不用字典,而是用列表存儲數據,故執行速度會較塊而且佔用較少內存, 針對須要建立大量對象的程序比較合適。

若是類繼承自使用__slots__的基類,那麼它也須要定義__slots__來存儲本身的屬性(即便不添加任何新屬性也是如此), 不然派生類的運行速度將更慢,比不用__slots__還糟。

__slots__還會破壞底層__dict__屬性,這對不少依靠__dict__來調試的代碼不利。

 

 

 

 (7)元類

元類主要用於在建立類時(不是建立實例),添加一些額外的檢查和收集關於類定義的信息,甚至能夠在建立類以前更改類定義的內容。這個屬於高級內容,通常用不到。

要理解元類,必須先理解用戶使用class語句定義新類時發生了些什麼事情。前面說過,類定義自己也是個對象,它是type類的實例。 下例展現了類建立時的僞代碼過程:

如下是用戶定義一個名爲Foo類的普通代碼:

class Foo(object):
    pass

如下是Python內部建立這個類定義對象的僞代碼過程:

class_name = "Foo"            # 類名
class_parents = (object,)     # 基類
class_body="""                # 類主體文本
    pass
"""
class_dict = {}
exec{class_body, globals(), class_dict)   # 在局部環境變量class_dict中執行類主體文本,以後類中全部的方法和類變量都會進入class_dict

# 最後一步:建立類對象Foo!
Foo = type(class_name, class_parents, class_dict)

所謂「元類」,就是用於建立和管理類對象的特殊對象,上例最後一句中,控制Foo建立的元類是一個名爲type的類。 最後一句把類名、基類列表、字典傳遞個元類的構造函數,以建立新的類對象。

上面建立類對象過程的最後一步:調用元類type()的步驟,能夠本身定義,這個就是「元類」的用法。能夠經過在基類元組中, 提供metaclass關鍵字參數來指定本身的元類。下面是一個使用元類的的例子,它要求全部方法在定義時必須擁有一個文檔字符串。

class DocMeta(type):
    def __init__(self, name, bases, dict):
        for key, value in dict.items():
            # 跳過特殊方法和私有方法:
            if key.startswith('__'): continue
            # 跳過不可調用的任何方法
            if not hasattr(value, '__call__'): continue
            # 檢查doc字符串
            if not getattr(value, '__doc__'): 
                raise TypeError('%s must have a docstring' %key)
        type.__init__(self, name, bases, dict)
            
# 要使用元類,必須先定義一個基類:
class Documented(metaclass=DocMeta):
    pass
  
# 而後將該基類用做全部須要這個檢查文檔字符串功能類的父類:
class Foo(Documented):
    def bar(self, a, b):
        """This is bar method document."""
        pass

 

下面是一個更高級的元類應用,它在建立類以前不止檢查,還會更改類定義的內容。這個經過定義元類的__new__()方法來實現。 下例展現了若是將元類和描述符結合使用。雖然使用元類能夠改變用戶定義的類,但不建議過多使用,由於會形成理解困難。

class TypedProperty:
    def __init__(self, name, type, default=None):
        self.name = None
        self.type = type
        self.default = default if default else type() 
        
    def __get__(self, instance, cls):
        return getattr(instance, self.name, self.default)   # 這個getattr是Python內置函數
        
    def __set__(self, instance, value):
        if not isinstance(value, self.type):
            raise TypeError("Must be a %s" %self.type)
        setattr(instance, self.name, value)                 # 這個setattr是Python內置函數
      
    def __delete__(self, instance):
        raise AttributeError("Cannot delete attribute")
        
class TypedMeta(type):
    def __new__(cls, name bases, dict):
        slots = []
        for key, value in dict.items():
            if isinstance(value, TypedProperty):
                value.name = "_" + key
                slots.append(value.name)
        dict['__slots__'] = slots
        return type.__new__(cls, name, bases, dict)
        
# 用戶要使用元類功能的基類定義
class Typed(metaclass=TypedMeta):
    pass
        
class Foo(Typed):
    name = TypedProperty(str)
    num = TypedProperty(int, 42)

 

 

 

 

返回目錄

相關文章
相關標籤/搜索