关于无意踩中的小坑的记录
__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
类型进行了相加, 而这种运算不被支持, 所以产生了类型报错. 但对此我有以下两个问题:
- 我的
person_list
列表只包含Person
类元素, 何来的int
类型? - 即便是
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)
。…
由此可见, 对加号的重载是存在方向性的. 也就是说, 在之前的例子里:
tom + 1
可以看作是tom.__add__(1)
, 此处的1
作为other
的参数可被我的方法进行正确处理, 从而抛出异常;- 在处理
sum(person_list) = 0 + tom + jack
时, 对0 + tom
可以看作0.__add__(tom)
, 此时没有针对这种情况的处理, 自然就直接报错了.
解决
为了解决上面的问题, 可以引入右操作符
的方法: __radd__
. 当x.__add__(y)
不被支持时(返回NotImplemented
), python会尝试调用y.__radd__(x)
方法, 只有当两者皆不存在时, 才会返回报错.
根据以上知识, 整体需修改的有以下两点:
- 在
__add__
方法中添加对int
的处理, 用于解决在sum()
中与数字0
相加的场景; - 实现
__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] 模拟数字类型类型