1.16运算符重载

基础知识

“运算符重载”只是意味着在某个类的方法中拦截内置的操作——当类的实例出现内置操作中时,python会自动调用你的方法,并且你的方法的返回值会作为相应操作的结果。

  • 运算符重载让类拦截常规的python操作。
  • 类可重载所有的python表达式运算符。
  • 类也可重载打印、函数调用、属性访问等内置运算。
  • 重载使类实例的行为更接近内置类型。
  • 重载是通过在一个类中提供特殊名称的方法来实现的。

一、构造函数和加减表达式:__init__和__add__和__sub__

class Number:
    def __init__(self, start):
        self.data = start

    def __add__(self, other):
        return Number(self.data + other)

    def __sub__(self, other):
        return Number(self.data - other)


if __name__ == "__main__":
    X = Number(5)
    print(X.data)
    Y = X + 1
    print(Y.data)
    Z = X - 1
    print(Z.data)

Numbr生成的调用现在会传入一个参数,这是传给__init__构造函数内的参数value的,并在构造函数中被赋值给self.data。此外,Number对象现在可以出现在 + 表达式和 - 表达式中。对于 +、-,Python把左侧的实例对象传给__add__中的self参数,而把右侧的对象传给other。__add__的返回值则成为 + 和 - 表达式的结果。

注意:从技术的角度讲,在一个实例被创建的过程中,首先触发的是__new__方法,这一方法将创建并返回一个新的实例对象,并传入__init__函数以供初始化。

二、索引和分片:__getitem__和__setitem__

  1. 如果一个类定义(或继承)了__getitem__的话,该方法就会在实例进行索引运算和分片运算的时候被调用。
class Indexer:
    data = [5, 6, 7, 8, 9]

    def __getitem__(self, index):
        print("getitem:", index)
        return self.data[index]


if __name__ == "__main__":
    X = Indexer()
    print("索引:")
    X[0]
    X[1]
    X[-1]
    print("分片:")
    print(X[2:4])
    print(X[1:])
    print(X[:-1])
    print(X[::2])
索引:
getitem: 0
getitem: 1
getitem: -1
分片:
getitem: slice(2, 4, None)
[7, 8]
getitem: slice(1, None, None)
[6, 7, 8, 9]
getitem: slice(None, -1, None)
[5, 6, 7, 8]
getitem: slice(None, None, 2)
[5, 7, 9]

当实例X出现在X[i]这样的索引运算中时,Python会调用这个实例继承的__getitem__方法,把X作为第一位参数传入,并且将方括号内的索引值传给第二个参数。
当实例X出现在X[i,j]这样的分片运算中时,Python会调用这个实例继承的__getitem__方法,把X作为第一位参数传入,第二个参数接收一个分片对象,如:slice(2, 4, None),它在一个新的索引表达式中直接传递给内嵌的列表索引。

>>> L = [5,6,7,8,9]
>>> L[2:4]
[7, 8]
>>> L[slice(2,4)]
[7, 8]

上面的代码可以看出,分片语法只不过就是分片对象来索引的语法糖。

  1. 如果使用了__setitem__索引赋值方法的话,它能拦截索引赋值和分片赋值。
class IndexSetter:
    data = [1, 2, 3, 4, 5, 6]

    def __setitem__(self, key, value):
        print("setitem:", key, value)
        self.data[key] = value


if __name__ == "__main__":
    X = IndexSetter()
    print("索引赋值:")
    X[0] = "a"
    X[1] = "b"
    print(X.data)
    print("分片赋值:")
    X[2:4] = ["c", "d"]
    print(X.data)
索引赋值:
setitem: 0 a
setitem: 1 b
['a', 'b', 3, 4, 5, 6]
分片赋值:
setitem: slice(2, 4, None) ['c', 'd']
['a', 'b', 'c', 'd', 5, 6]

三、可迭代对象:__iter__和__next__

在python中,迭代是迭代上下文先将一个可迭代对象传入内置函数iter,并尝试调用__iter__方法来实现的,而这种方法应该返回一个迭代器对象。如果提供了这种方法,在使用迭代工具for时,Python就会重复调用这个迭代器对象的__next__方法来产生元素,直到引发StopIteation异常。对于手动迭代来说,可以使用一个next内置函数:next(x)与I.next()相同。我们可以自定义一个对象来模拟这个情景。
例如:用一个类来定义用户自定义可迭代对象,从按需生成所需要的值,而不是一次生成所有的值。

class Squares:
    def __init__(self, start, stop):
        self.value = start - 1
        self.stop = stop

    def __iter__(self):
        print("__iter__")
        return self

    def __next__(self):
        print("__next__")
        if self.value == self.stop:
            raise StopIteration
        self.value += 1
        return self.value


if __name__ == "__main__":
    s = Squares(1, 5)
    x = iter(s)
    print("-------")
    print(next(x))
    print(next(x))
    print(next(x))
    print(next(x))
    print(next(x))
__iter__
-------
__next__
1
__next__
2
__next__
3
__next__
4
__next__
5

我们注意到一个类的__iter__被设计为只调用一次,而不是多次调用。由于当前的Squares类的iter通常返回只带有一份迭代状态复制的self,因此这是一个一次性的迭代;一旦你迭代过这个类中的一个实例,它就变空。对该实例再次调用__iter__则还是会返回self,以及所剩下的最终状态,通常许需要为每一次新的迭代创建一个新的可迭代对象。

四、属性访问:__gettattr__和__setattr__

在python中,类在需要的时候也可以拦截基本的属性访问(也称为点号运算)。具体来说,对于一个类中创建的object,点号运算表达式object.attribute也可以用你的代码。来实现,用于属性引用、赋值操作或删除操作。

  1. 属性引用
    __getattr__方法用来拦截属性引用。每当你用一个未定义的(不存在的)属性名称字符串对一个实例对象做点号运算时,它就会被调用。如果python能通过其继承搜索过程找到这个属性,那么该方法就不会被调用。
    例如:下面这个类会捕获属性引用,为其中一个属性动态的计算值,并为其他不被支持的属性引发错误。
class Empty:
    def __getattr__(self, attrname):
        if attrname == "age":
            return 40
        else:
            raise AttributeError(attrname)


if __name__ == "__main__":
    x = Empty()
    print(x.age)
    print(x.name)

% getattrfunc.py

40
AttributeError: name

在这里,empty类和实例x本身并没有属性,所以对x.age属性的访问会被路由到__getattr__方法。其中self被赋值为实例(x),而attrname则被赋值为未定义的属性名称字符串('age')。这个类传回一个实际值作为x.age点号表达式的结果(40),这让age看上去就像真实的属性。对于类不知道该如何处理的属性,__getattr__会引发内置的AttributeErrot异常。

  1. 属性赋值和删除
    __setattr__会拦截所有的属性赋值。self.attr=value会变成self.__setattr__('attr',value)。不过在使用的时候需要一点技巧,因为在__setattr__中队任何self属性做赋值,都将再次掉用__setattr__,这就潜在导致了无穷递归循环。在类中所有赋值都会被路由到__setattr__。可以把实例属性的赋值编写成对属性字典键的赋值来避免循环。也就是说,是使用self.__dict__['name'] = x,而不是self.name = x。
class Accesscontrol:
    def __setattr__(self, attr, value):
        if attr == "age":
            self.__dict__[attr] = value
        else:
            raise AttributeError(attr + "not allowed")


if __name__ == "__main__":
    x = Accesscontrol()
    x.age = 40
    print(x.age)
    x.name = 'Bob'

五、调用表达式:__call__

带有一个__call__的类和实例能够支持与常规函数和方法完全相同的参数语法和语义。换一种说法,__call__方法支持所有的函数中参数传递模式——传递给实例的所有信息都会传递给该方法,包括通常隐含的self实例参数。

class Callee:
    def __call__(self, *pargs, **kwargs):
        print("Called:", pargs, kwargs)


if __name__ == "__main__":
    c = Callee()
    c(1, 2, 3)
    c(1, 2, 3, x=4, y=5)

% python callfunc.py

Called: (1, 2, 3) {}
Called: (1, 2, 3) {'x': 4, 'y': 5}

一些知识点:当一个类实例化时(创建一个对象)执行__call__方法,__call__方法的返回值就是实例化的对象。__call__内部调用(1)__new__方法,创建一个对象;(2)__init__方法,初始化对象。
__new__至少要有一个参数cls,代表要实例化的类,此参数在实例化时由Python解释器自动提供

__new__必须要有返回值,返回实例化出来的实例,这点在自己实现__new__时要特别注意,可以return父类__new__出来的实例,或者直接是object的__new__出来的实例

__init__有一个参数self,就是这个__new__返回的实例,__init__在__new__的基础上可以完成一些其它初始化的动作,__init__不需要返回值

六、对象析构函数:__del__

每当实例产生时调用__init__构造函数(注意__new__先于__运行并创建出对象)。反过来,当实例空间被收回时(也称作“垃圾收集”),它的对立面__del__,也就是析构函数方法,就会自动执行。

class Life:
    def __init__(self, name='unknown'):
        print("Hello" + name)
        self.name = name

    def live(self):
        print(self.name)

    def __del__(self):
        print("Goodbly", self.name)


if __name__ == "__main__":
    brian = Life('Brian')
    brian.live()
    brian = "loretta"

% python lifefunc.py

HelloBrian
Brian
Goodbly Brian

在这里,当brian被赋值为一个字符串的时,我们会失去Life实实例的最后一个引用并因此触发其析构函数。

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