最近在看一些项目源码,对python的魔术方法一直有点迷糊,所以花了些时间,认真研究了一下,总结在这里。

魔术方法

在python中,经常看到一个用__包裹起来的方法,这些方法被称为魔术方法或特殊方法。最常见如__init__,对实例属性进行初始化。

new

在 Python 中,当我们创建一个类的实例时,类会先调用 __new__(cls[, ...]) 来创建实例,然后 __init__ 方法再对该实例(self)进行初始化。 关于 newinit 有几点需要注意:

  • new 是在 init 之前被调用的;
  • new 是类方法,init 是实例方法;
  • 重载 new 方法,需要返回类的实例;

定义一个类A,重载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
# 创建一个实例a1
> a1 = A()
NEW
INIT
# 创建一个实例a2失败,直接返回a1
> a2 = A()
EXISTS
INIT
# 清空dict
> A._dict.clear()
# 创建一个实例a3成功
> a3 = A()
NEW
INIT
# 查看dict内容
> A._dict
{'key': <__main__.A at 0x10834ab70>}

str&repr

这两个魔法方法一般会放到一起进行讲解,它们的主要差别为:

  • __str__强调可读性,而__repr__强调准确性/标准性
  • __str__的目标人群是用户,而__repr__的目标人群是机器,它的结果是可以被执行的
  • %s调用__str__方法,而%r调用__repr__方法
# 第一版
class Foo(object):
    def __init__(self, name):
        self.name = name
> Foo('boheyan')
<__main__.Foo at 0x10834c160>
# 第二版
class Foo(object):
    def __init__(self, name):
        self.name = name
    # 创建一个__str__方法    
    def __str__(self):
        return 'Foo object (name: %s)' % self.name
> Foo('boheyan')
<__main__.Foo at 0x108341fd0>
> str(Foo('boheyan'))
Foo object (name: boheyan)
# 第三版
class Foo(object):
    def __init__(self, name):
        self.name = name

    def __str__(self):
        return 'Foo object (name: %s)' % self.name
    # 创建一个__repr__方法,为了简单,直接赋值
    __repr__ = __str__
> Foo('boheyan')
Foo object (name: boheyan)
> str(Foo('boheyan'))
Foo object (name: boheyan)

这里值得注意的是,如果只定义了strrepr其中一个,那会是什么结果?

  • 如果只定义了__str__,那么repr(person)输出<__main__.Person object at 0x1093642d0>
  • 如果只定义了__repr__,那么str(person)与repr(person)结果是相同的 也就是说,__repr__在表示类时,是一级的,如果只定义它,那么__str__ = __repr__

iter

在某些情况下,我们希望实例对象可被用于 for...in 循环,这时我们需要在类中定义 __iter____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, end=' ') 
> 1 1 2 3 5 8

getitem

有时,我们希望可以使用 obj[n] 这种方式对实例对象进行取值,比如对斐波那契数列,我们希望可以取出其中的某一项,这时我们需要在类中实现 __getitem__ 方法,比如下面的例子:

class Fib(object):
    def __getitem__(self, n):
        a, b = 1, 1
        for x in range(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)

如果想进一步实现切片功能:

class Fib(object):
    def __getitem__(self, n):
        if isinstance(n, slice):   # 如果 n 是 slice 对象
            a, b = 1, 1
            start, stop = n.start, n.stop
            step = n.step
            L = []
            for i in range(stop):
                if i >= start:
                    L.append(a)
                a, b = b, a + b
            if step:              # 如果有step,进一步处理
                lst = []
                n = len(L) // step
                for i in range(n+1):
                    lst.append(L[i*step])
                return lst
            return L            # 没有step,直接返回L

        if isinstance(n, int):     # 如果 n 是 int 型
            a, b = 1, 1
            for i in range(n):
                a, b = b, a + b
            return a
> fib = Fib()
> fib[3:10]
[3, 5, 8, 13, 21, 34, 55]
> fib[3:10:3]
[3, 13, 55]

__geitem__ 用于获取值,类似地,__setitem__ 用于设置值,__delitem__ 用于删除值

getattr

__getattr__ 在属性不存在的情况下会被调用,对已存在的属性不会调用 __getattr__ __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), {})
> print(p.x, p.y, p.z)
3 4 0
> p.w = 5
call func set attr (('w', 5), {})
> print(p.w)
5
> del p.w
call func del attr (('w',), {})

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)
> p(5)              # # 传入参数,对实例进行调用,对应 p.__call__(5)
12

统计

魔法方法 什么时候被调用 解释
new(cls [,...]) instance = MyClass(arg1, arg2) new在实例创建时调用
init(self [,...]) instance = MyClass(arg1,arg2) init在实例创建时调用
cmp(self) self == other, self > other 等 进行比较时调用
pos(self) +self 一元加法符号
neg(self) -self 一元减法符号
invert(self) ~self 按位取反
index(self) x[self] 当对象用于索引时
nonzero(self) bool(self) 对象的布尔值
getattr(self, name) self.name #name不存在 访问不存在的属性
setattr(self, name) self.name = val 给属性赋值
_delattr(self, name) del self.name 删除属性
getattribute(self,name) self.name 访问任意属性
getitem(self, key) self[key] 使用索引访问某个元素
setitem(self, key) self[key] = val 使用索引给某个元素赋值
delitem(self, key) del self[key] 使用索引删除某个对象
iter(self) for x in self 迭代
contains(self, value) value in self, value not in self 使用in进行成员测试
call(self [,...]) self(args) “调用”一个实例
enter(self) with self as x: with声明的上下文管理器
exit(self, exc, val, trace) with self as x: with声明的上下文管理器
getstate(self) pickle.dump(pkl_file, self) Pickling
setstate(self) data = pickle.load(pkl_file) Pickling

参考

http://funhacks.net/explore-python/Class/w.html http://kaito-kidd.com/2017/02/22/python-magic-methods/ http://pyzh.readthedocs.io/en/latest/python-magic-methods-guide.html