python 定製類和魔法方法

在 Python 中,咱們能夠常常看到以雙下劃線 __ 包裹起來的方法,好比最多見的 __init__,這些方法被稱爲魔法方法(magic method)或特殊方法(special method)。簡單地說,這些方法能夠給 Python 的類提供特殊功能,方便咱們定製一個類,好比 __init__ 方法能夠對實例屬性進行初始化。html

完整的特殊方法列表可在這裏查看,本文介紹部分經常使用的特殊方法:python

  • __new__
  • __str__ , __repr__
  • __iter__
  • __getitem__ , __setitem__ , __delitem__
  • __getattr__ , __setattr__ , __delattr__
  • __call__

new

在 Python 中,當咱們建立一個類的實例時,類會先調用 __new__(cls[, ...]) 來建立實例,而後 __init__ 方法再對該實例(self)進行初始化。app

關於 __new__ 和 __init__ 有幾點須要注意:函數

  • __new__ 是在 __init__ 以前被調用的;
  • __new__ 是類方法,__init__ 是實例方法;
  • 重載 __new__ 方法,須要返回類的實例

通常狀況下,咱們不須要重載 __new__ 方法。但在某些狀況下,咱們想控制實例的建立過程,這時能夠經過重載 __new_ 方法來實現。spa

讓咱們看一個例子:code

class A(object):
    _dict = dict()

    def __new__(cls):
        if 'key' in A._dict:
            print "EXISTS"
            return A._dict['key']
        else:
            print "NEW"
            return object.__new__(cls)

    def __init__(self):
        print "INIT"
        A._dict['key'] = self

在上面,咱們定義了一個類 A,並重載了 __new__ 方法:當 key 在 A._dict 中時,直接返回 A._dict['key'],不然建立實例。htm

執行狀況:對象

>>> a1 = A()
NEW
INIT
>>> a2 = A()
EXISTS
INIT
>>> a3 = A()
EXISTS
INIT

str & repr

先看一個簡單的例子:ip

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

>>> print Foo('ethan')
<__main__.Foo object at 0x10c37aa50>

在上面,咱們使用 print 打印一個實例對象,但若是咱們想打印更多信息呢,好比把 name 也打印出來,這時,咱們能夠在類中加入 __str__ 方法,以下:ci

class Foo(object):
    def __init__(self, name):
        self.name = name
    def __str__(self):
        return 'Foo object (name: %s)' % self.name

>>> print Foo('ethan')      # 使用 print
Foo object (name: ethan)
>>>
>>> str(Foo('ethan'))       # 使用 str
'Foo object (name: ethan)'
>>>
>>> Foo('ethan')             # 直接顯示
<__main__.Foo at 0x10c37a490>

能夠看到,使用 print 和 str 輸出的是 __str__ 方法返回的內容,但若是直接顯示則不是,那能不能修改它的輸出呢?固然能夠,咱們只需在類中加入 __repr__ 方法,好比:

class Foo(object):
    def __init__(self, name):
        self.name = name
    def __str__(self):
        return 'Foo object (name: %s)' % self.name
    def __repr__(self):
        return 'Foo object (name: %s)' % self.name

>>> Foo('ethan')
'Foo object (name: ethan)'

能夠看到,如今直接使用 Foo('ethan') 也能夠顯示咱們想要的結果了,然而,咱們發現上面的代碼中,__str__ 和 __repr__ 方法的代碼是同樣的,能不能精簡一點呢,固然能夠,以下:

class Foo(object):
    def __init__(self, name):
        self.name = name
    def __str__(self):
        return 'Foo object (name: %s)' % self.name
    __repr__ = __str__

iter

在某些狀況下,咱們但願實例對象可被用於 for...in 循環,這時咱們須要在類中定義 __iter__ 和 next(在 Python3 中是 __next__)方法,其中,__iter__ 返回一個迭代對象,next 返回容器的下一個元素,在沒有後續元素時拋出 StopIteration 異常。

看一個斐波那契數列的例子:

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
        return self.a    

>>> fib = Fib()
>>> for i in fib:
...     if i > 10:
...         break
...     print i
...
1
1
2
3
5
8

getitem

有時,咱們但願可使用 obj[n] 這種方式對實例對象進行取值,好比對斐波那契數列,咱們但願能夠取出其中的某一項,這時咱們須要在類中實現 __getitem__ 方法,好比下面的例子:

class Fib(object):
    def __getitem__(self, n):
        a, b = 1, 1
        for x in xrange(n):
            a, b = b, a + b
        return a

>>> fib = Fib()
>>> fib[0], fib[1], fib[2], fib[3], fib[4], fib[5]
(1, 1, 2, 3, 5, 8)

咱們還想更進一步,但願支持 obj[1:3] 這種切片方法來取值,這時 __getitem__ 方法傳入的參數多是一個整數,也多是一個切片對象 slice,所以,咱們須要對傳入的參數進行判斷,可使用 isinstance進行判斷,改後的代碼以下:

class Fib(object):
    def __getitem__(self, n):
        if isinstance(n, slice):   # 若是 n 是 slice 對象
            a, b = 1, 1
            start, stop = n.start, n.stop
            L = []
            for i in xrange(stop):
                if i >= start:
                    L.append(a)
                a, b = b, a + b
            return L
        if isinstance(n, int):     # 若是 n 是 int 型
            a, b = 1, 1
            for i in xrange(n):
                a, b = b, a + b
            return a

如今,咱們試試用切片方法:

>>> fib = Fib()
>>> fib[0:3]
[1, 1, 2]
>>> fib[2:6]
[2, 3, 5, 8]

上面,咱們只是簡單地演示了 getitem 的操做,可是它還很不完善,好比沒有對負數處理,不支持帶 step 參數的切片操做 obj[1:2:5] 等等,讀者有興趣的話能夠本身實現看看。

__geitem__ 用於獲取值,相似地,__setitem__ 用於設置值,__delitem__ 用於刪除值,讓咱們看下面一個例子:

class Point(object):
    def __init__(self):
        self.coordinate = {}

    def __str__(self):
        return "point(%s)" % self.coordinate

    def __getitem__(self, key):
        return self.coordinate.get(key)

    def __setitem__(self, key, value):
        self.coordinate[key] = value

    def __delitem__(self, key):
        del self.coordinate[key]
        print 'delete %s' % key

    def __len__(self):
        return len(self.coordinate)

    __repr__ = __str__

在上面,咱們定義了一個 Point 類,它有一個屬性 coordinate(座標),是一個字典,讓咱們看看使用:

>>> p = Point()
>>> p['x'] = 2    # 對應於 p.__setitem__('x', 2)
>>> p['y'] = 5    # 對應於 p.__setitem__('y', 5)
>>> p             # 對應於 __repr__
point({'y': 5, 'x': 2})
>>> len(p)        # 對應於 p.__len__
2
>>> p['x']        # 對應於 p.__getitem__('x')
2
>>> p['y']        # 對應於 p.__getitem__('y')
5
>>> del p['x']    # 對應於 p.__delitem__('x')
delete x
>>> p
point({'y': 5})
>>> len(p)
1

getattr

當咱們獲取對象的某個屬性,若是該屬性不存在,會拋出 AttributeError 異常,好比:

class Point(object):
    def __init__(self, x=0, y=0):
        self.x = x
        self.y = y

>>> p = Point(3, 4)
>>> p.x, p.y
(3, 4)
>>> p.z
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-547-6dce4e43e15c> in <module>()
----> 1 p.z

AttributeError: 'Point' object has no attribute 'z'

那有沒有辦法不讓它拋出異常呢?固然有,只需在類的定義中加入 __getattr__ 方法,好比:

class Point(object):
    def __init__(self, x=0, y=0):
        self.x = x
        self.y = y
    def __getattr__(self, attr):
        if attr == 'z':
            return 0

>>> p = Point(3, 4)
>>> p.z
0

如今,當咱們調用不存在的屬性(好比 z)時,解釋器就會試圖調用 __getattr__(self, 'z') 來獲取值,可是,上面的實現還有一個問題,當咱們調用其餘屬性,好比 w ,會返回 None,由於 __getattr__ 默認返回就是 None,只有當 attr 等於 'z' 時才返回 0,若是咱們想讓 __getattr__ 只響應幾個特定的屬性,能夠加入異常處理,修改 __getattr__ 方法,以下:

def __getattr__(self, attr):
    if attr == 'z':
        return 0
    raise AttributeError("Point object has no attribute %s" % attr)

這裏再強調一點,__getattr__ 只有在屬性不存在的狀況下才會被調用,對已存在的屬性不會調用 __getattr__

與 __getattr__ 一塊兒使用的還有 __setattr____delattr__,相似 obj.attr = valuedel obj.attr,看下面一個例子:

class Point(object):
    def __init__(self, x=0, y=0):
        self.x = x
        self.y = y

    def __getattr__(self, attr):
        if attr == 'z':
            return 0
        raise AttributeError("Point object has no attribute %s" % attr)

    def __setattr__(self, *args, **kwargs):  
        print 'call func set attr (%s, %s)' % (args, kwargs)
        return object.__setattr__(self, *args, **kwargs)

    def __delattr__(self, *args, **kwargs):  
        print 'call func del attr (%s, %s)' % (args, kwargs)
        return object.__delattr__(self, *args, **kwargs)

>>> p = Point(3, 4)
call func set attr (('x', 3), {})
call func set attr (('y', 4), {})
>>> p.z
0
>>> p.z = 7
call func set attr (('z', 7), {})
>>> p.z
7
>>> p.w
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 8, in __getattr__
AttributeError: Point object has no attribute w
>>> p.w = 8
call func set attr (('w', 8), {})
>>> p.w
8
>>> del p.w
call func del attr (('w',), {})
>>> p.__dict__
{'y': 4, 'x': 3, 'z': 7}

call

咱們通常使用 obj.method() 來調用對象的方法,那能不能直接在實例自己上調用呢?在 Python 中,只要咱們在類中定義 __call__ 方法,就能夠對實例進行調用,好比下面的例子:

class Point(object):
    def __init__(self, x, y):
        self.x, self.y = x, y
    def __call__(self, z):
        return self.x + self.y + z

使用以下:

>>> p = Point(3, 4)
>>> callable(p)     # 使用 callable 判斷對象是否能被調用
True
>>> p(6)            # 傳入參數,對實例進行調用,對應 p.__call__(6)
13                  # 3+4+6

能夠看到,對實例進行調用就好像對函數調用同樣。

小結

  • __new__ 在 __init__ 以前被調用,用來建立實例。
  • __str__ 是用 print 和 str 顯示的結果,__repr__ 是直接顯示的結果。
  • __getitem__ 用相似 obj[key] 的方式對對象進行取值
  • __getattr__ 用於獲取不存在的屬性 obj.attr
  • __call__ 使得能夠對實例進行調用
相關文章
相關標籤/搜索
本站公眾號
   歡迎關注本站公眾號,獲取更多信息