美文网首页
1.16运算符重载

1.16运算符重载

作者: Benedict清水 | 来源:发表于2020-06-10 15:30 被阅读0次

基础知识

“运算符重载”只是意味着在某个类的方法中拦截内置的操作——当类的实例出现内置操作中时,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实实例的最后一个引用并因此触发其析构函数。

相关文章

  • 1.16运算符重载

    基础知识 “运算符重载”只是意味着在某个类的方法中拦截内置的操作——当类的实例出现内置操作中时,python会自动...

  • 运算符重载及其他约定

    7.1 重载算术运算符 重载二元算术运算符 重载复合赋值运算符 重载一元运算符 重载比较运算符 等号运算符:‘eq...

  • C++ 部分运算符重载

    可重载的运算符 不可重载的运算符和符号 重载运算符为类的成员函数 重载运算符为友元函数 重载赋值运算符 重载流插入...

  • 2019-07-11 运算符的重载

    运算符的重载 重载运算符 (“::”,“.*”,“.”,“?:”)不能被重载。 重载运算符时,不能改变其本质,如不...

  • 运算符重载

    一.+号运算符重载 二.<<号运算符重载 三.指针运算符重载(智能指针)

  • C++运算符重载-下篇 (Boolan)

    C++运算符重载-下篇 (Boolan) 本章内容:1. 运算符重载的概述2. 重载算术运算符3. 重载按位运算符...

  • C++运算符重载-上篇 (Boolan)

    C++运算符重载-上篇 (Boolan) 本章内容:1. 运算符重载的概述2. 重载算术运算符3. 重载按位运算符...

  • C++重载

    重载 C++语言规定: 重载的运算符要保持原运算符的意义。只能对已有的运算符重载,不能增加新的运算符。重载的运算符...

  • C++运算符重载详解

    运算符重载规则 1.被重载的运算符必须是已经存在的C++运算符,不能重载自己创建的运算符; 2.运算符被重载之后,...

  • 第十一章 使用类

    运算符重载 运算符重载是一种形式的C++多态。运算符重载将重载的概念扩展到运算符上,允许赋予C++运算符多种含义。...

网友评论

      本文标题:1.16运算符重载

      本文链接:https://www.haomeiwen.com/subject/imkutktx.html