Python中的__add__方法和sum()函数: 自定义类的求和实现

关于无意踩中的小坑的记录

__add__sum()方法简介

  • __add__是python中的一个魔术方法(magic method), 可以重载加号+的功能. 比如通过实现该方法, 可以进行类之间的加法:

    # naive_person.py
    class Person:
        def __init__(self, name: str, age: int) -> None:
            self.name = name
            self.age = age
    
        def __add__(self, other: any) -> 'Person':
            """
            定义Person类之间的加法为name变量的拼接, 以及年龄的总和,
            并以此返回一个新的Person类
            """
            if isinstance(other, Person):
                return Person(f'{self.name}+{other.name}', self.age + other.age)
            else:
                raise NotImplementedError('暂不支持Person类以外的加法')
    
        def __str__(self) -> str:
            return f'Person(Name={self.name}, Age={self.age})'
    
    
    if __name__ == '__main__':
        tom = Person('Tom', 21)
        jack = Person('Jack', 30)
        print(tom + jack)
    

    运行效果如下:

    >> python naive_person.py 
    Person(Name=Tom+Jack, Age=51)
    
  • 另一方面, python中的内置函数sum(), 用于将一个可迭代对象(比如列表list)中的数字元素进行求和:

    In [1]: a = [1, 2, 3]
    
    In [2]: sum(a)
    Out[2]: 6
    

问题描述

对于上面的Person类, 虽然不是数字, 但我想既然实现了__add__方法, 应该可以直接调用sum()方法进行整体的Person类之间的求和:

# naive_python.py
# ...Person class code

if __name__ == '__main__':
    tom = Person('Tom', 21)
    jack = Person('Jack', 30)
    person_list = [tom, jack]
    print(sum(person_list))

尝试运行:

Traceback (most recent call last):
  File "/home/neowell/project/blog/custom_sum/naive_person.py", line 22, in <module>
    print(sum(person_list))
TypeError: unsupported operand type(s) for +: 'int' and 'Person'

发生TypeError, 根据提示, 似乎是我尝试将int类型与Person类型进行了相加, 而这种运算不被支持, 所以产生了类型报错. 但对此我有以下两个问题:

  1. 我的person_list列表只包含Person类元素, 何来的int类型?
  2. 即便是int类型, 我自己已在__add__中进行了处理, 会抛出一个NotImplementedError的异常比如:
    # naive_person.py
    # ...Person class code
    if __name__ == '__main__':
        tom = Person('Tom', 21)
        jack = Person('Jack', 30)
        print(tom + 1)
    
    运行:
    Traceback (most recent call last):
    File "/home/neowell/project/blog/custom_sum/naive_person.py", line 24, in <module>
        print(tom + 1)
    File "/home/neowell/project/blog/custom_sum/naive_person.py", line 15, in __add__
        raise NotImplementedError('暂不支持Person类以外的加法')
    NotImplementedError: 暂不支持Person类以外的加法
    
    此处正确处理了异常, 那么为何在sum()中却没有被处理?

解释

关于sum()方法

关于sum()方法, 在文档中1的描述为:

sum(iterable, /, start=0)

start 开始自左向右对 iterable 的项求和并返回总计值。 iterable 的项通常为数字,而 start 值则不允许为字符串。

即当调用sum()时, 其实是有一个初始参数0的, 想来也是, sum()用于数之间的求和, 但如果输入元素只有一位, 那么需要有另外一个初始数才能进行加法运算:

In [1]: a = [1]

In [2]: sum(a)
Out[2]: 1

那么前面的示例中, 可以把求和式按这样展开: sum(person_list) = 0 + tom + jack.
当然, 虽然这样展开了, 仍没法解释为何没能正确抛出异常, 而这就涉及到对象与运算符的位置关系了, 即sum()文档提到的"自左向右", 为此, 需要重新来看下__add__这个方法.

关于__add__

对于__add__方法, 也被称作左操作数(left operand), 即把调用方法的对象放在符号的左边.
在文档2中, 关于模拟数字类型的说明里恰好拿__add__进行了举例:

…例如,求表达式 x + y的值,其中 x 是具有__add__()方法的类的一个实例,则会调用 x.__add__(y)。…

由此可见, 对加号的重载是存在方向性的. 也就是说, 在之前的例子里:

  1. tom + 1可以看作是tom.__add__(1), 此处的1作为other的参数可被我的方法进行正确处理, 从而抛出异常;
  2. 在处理sum(person_list) = 0 + tom + jack时, 对0 + tom可以看作0.__add__(tom), 此时没有针对这种情况的处理, 自然就直接报错了.

解决

为了解决上面的问题, 可以引入右操作符的方法: __radd__. 当x.__add__(y)不被支持时(返回NotImplemented), python会尝试调用y.__radd__(x)方法, 只有当两者皆不存在时, 才会返回报错.

根据以上知识, 整体需修改的有以下两点:

  1. __add__方法中添加对int的处理, 用于解决在sum()中与数字0相加的场景;
  2. 实现__radd__方法, 使计算0 + tom时, 最终转变为调用tom.__radd__(0)方法.

最后的文件如下:

# person.py
class Person:
    def __init__(self, name: str, age: int) -> None:
        self.name = name
        self.age = age

    def __add__(self, other: any) -> 'Person':
        """
        定义Person类之间的加法为name变量的拼接, 以及年龄的总和,
        并以此返回一个新的Person类
        """
        if isinstance(other, Person):
            return Person(f'{self.name}+{other.name}', self.age + other.age)
        elif isinstance(other, int):
            # 添加对int类型的处理
            return self
        else:
            raise NotImplementedError('暂不支持Person类以外的加法')

    def __radd__(self, other: any) -> 'Person':
        """
        添加右操作方法
        """
        return self.__add__(other)

    def __str__(self) -> str:
        return f'Person(Name={self.name}, Age={self.age})'


if __name__ == '__main__':
    tom = Person('Tom', 21)
    jack = Person('Jack', 30)
    fancy = Person('fancy', 40)
    names = [tom, jack, fancy]
    res = sum(names)
    print(res)

运行:

>> python person.py 
Person(Name=Tom+Jack+fancy, Age=91)

总结

上述方法通过__add____radd__实现了对sum()方法表现的改变, 当然这个类的实现很简陋, 想要用于实际还需要很多更细致的处理. 除非涉及到各种运算符的重载, 不然自行在类里实现一个自定义的累加方法反而更方便些.

参考

[1] sum方法说明

[2] 模拟数字类型类型

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值