python的多种魔术方法

193 阅读5分钟

[toc] 定制类和魔法方法

  • new
  • str , repr
  • iter
  • getitem , setitem , delitem
  • getattr , setattr , delattr
  • call

new

在 Python 中,当我们创建一个类的实例时,类会先调用 new(cls[, ...]) 来创建实例,然后 init 方法再对该实例(self)进行初始化。

关于 newinit 有几点需要注意: new 是在 init 之前被调用的; new 是类方法,init 是实例方法; 重载 new 方法,需要返回类的实例; 一般情况下,我们不需要重载 new 方法。但在某些情况下,我们想控制实例的创建过程,这时可以通过重载 _new 方法来实现。

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

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

print Foo('ethan') # 使用 print Foo object (name: ethan)

str(Foo('ethan')) # 使用 str 'Foo object (name: ethan)'

Foo('ethan') # 直接显示 <main.Foo at 0x10c37a490> Foo('ethan') # 使用repr(类中实现) 'Foo object (name: ethan)'

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

getitem、setitem、delitem

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、setattr、delattr

当我们获取对象的某个属性,如果该属性不存在,会抛出 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)

setattr, delattr

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

slots

在 Python 中,我们在定义类的时候可以定义属性和方法。当我们创建了一个类的实例后,我们还可以给该实例绑定任意新的属性和方法。 看下面一个简单的例子:

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

>>> p = Point(3, 4)
>>> p.z = 5    # 绑定了一个新的属性
>>> p.z
5
>>> p.__dict__
{'x': 3, 'y': 4, 'z': 5}

在上面,我们创建了实例 p 之后,给它绑定了一个新的属性 z,这种动态绑定的功能虽然很有用,但它的代价是消耗了更多的内存。

因此,为了不浪费内存,可以使用 slots 来告诉 Python 只给一个固定集合的属性分配空间,对上面的代码做一点改进,如下:

class Point(object):
    __slots__ = ('x', 'y')       # 只允许使用 x 和 y

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

上面,我们给 slots 设置了一个元组,来限制类能添加的属性。现在,如果我们想绑定一个新的属性,比如 z,就会出错了,如下:

>>> p = Point(3, 4)
>>> p.z = 5
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-648-625ed954d865> in <module>()
----> 1 p.z = 5

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

注意: 使用 slots 有一点需要注意的是,slots 设置的属性仅对当前类有效,对继承的子类不起效,除非子类也定义了 slots,这样,子类允许定义的属性就是自身的 slots 加上父类的 slots。