Python的魔术方法

在 Python 中,我们可以经常看到以双下划线 __ 包裹起来的方法,比如最常见的 __init__,这些方法被称为魔法方法(magic method)或特殊方法(special method)。简单地说,这些方法可以给 Python 的类提供特殊功能,方便我们定制一个类,比如 __init__ 方法可以对实例属性进行初始化。

完整的特殊方法列表可在这里查看,本文就一些常见的方法简单做一个说明,方便记忆:

1、call

之前一直以Java作为第一工作语言,初始在接触Django源码时,经常会遇到一个问题,就是:卧槽,这怎么调过来的??习惯了Java语言Function to Next Funcation的分析步骤,突然间发现没有任何显式调用方法,这个Object怎么就执行了,这个时候就不得不提一下__call__方法,本文就这个方法简单做一个说明,方便记忆~

python中所有的东西都被称为对象,对象分为可以被调用和不可以被调用,可调用对象:即是任何通过函数操作符()来调用的对象,例如函数,函数是对象,可以被调用:

def foo(x):
    return x

if __name__ == '__main__':   
    a = foo(3) 
    print(a)   # 3
    print(callable(a))  # False
    print(callable(foo)) # True

但是如果是类的话会发生什么情况呢?Python给类提供了名为__call__的特别方法,该方法允许程序员创建可调用的对象(实例)。默认情况下,__call__方法是没有实现的,这意味着大多数情况下实例是不可调用的。

class Test(object):
    def method(self):
        return 1

if __name__ == '__main__':
    a = Test()
    print(callable(a)) # False

如何解决这一个问题?这里就用到call函数

class Test(object):
    def __call__(self):
        return 1

if __name__ == '__main__':
    a = Test()
    print(callable(a)) # True
    print(a())  # 1

任何设计都是为了解决问题而存在的,那么__call__方法的用途是什么呢?__call__在那些类的实例经常改变状态的时候会非常有效。调用这个实例是一种改变这个对象状态的直接和优雅的做法:


class Entity:
    """调用实体来改变实体的位置"""
    def __init__(self, size, x, y):
        self.x, self.y = x, y
        self.size = size
  
    def __call__(self, x, y):
        """改变实体的位置"""
        self.x, self.y = x, y

if __name__ == '__main__':
    e = Entity(1, 2, 3)  # 创建实例
    e(4, 5)  # 实例可以象函数那样执行,并传入x y值,修改对象的x y

2、new & init

1)__new__

  • __new__调用以创建一个 cls 类的新实例;

  • 如果 __new__未返回一个 cls 的实例,则新实例的 __init__()方法就不会被执行;

  • __new__的目的主要是允许不可变类型的子类 (例如 int, str 或 tuple) 定制实例创建过程。

2)__init__

  • 在实例通过 __new__被创建之后,返回调用者之前调用;
  • 对象是由 __new____init__协作构造完成的

3、str & repr

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

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

在上面的例子中,我们使用 print 打印一个实例对象,但如果我们想打印更多信息呢,比如把 name 也打印出来,这时,我们可以在类中加入 __str__ 方法,如下:

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__

4、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

5、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

6、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 = value, del 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}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342

推荐阅读更多精彩内容