关于运算符重载的原理问题 求大神

2020-08-28 14:30:23 +08:00
uswood  uswood

目前学到运算符重载的部分,书中内容只做了例子,但关于原理有三点疑问弄不明白:

代码部分:

class Test:
	def __init__(self, val):
		self.val = val
	def __add__(self, other):
		print('add', self.val, other)
		return self.val + other

问题部分:

1 、定义运算符重载的时候,它的编写是有规则的吗? 例如定义__add__,一定是接受 2 个参数(self, other),一定是 return self.val + other 吗?是否每种运算符重载都有特定的编写规则?还比如__getitem__重载,书中例子写的是:

class Test:
	def __getitem__(self, i):
		return self.data[1]

2 、为什么要这么写? 如果只是想让自定义的类拥有迭代的功能难道只写 def __getitem(self): pass 就可以了吗?

3 、在定义的__add__里面,也只是写了“+”这个符号,我的理解是:怎么“加”的是 python 本身封装好了的,运算符重载的用处只是在为了让用户定义的类拥有“+”的运算,顶多让你自定义在“+”的基础上还做些什么。这样理解对不对, 关于运算符重载能举比较实际点的用处吗?

2715 次点击
所在节点   Python  Python
23 条回复
no1xsyzy
no1xsyzy
2020-08-28 14:48:26 +08:00
1 、 这类魔法方法一般有固定的参数数量,但你只要保证 sub.__add__(obj) 能够正常调用即可,换句话说,允许采用 def __add__(self, other, magic_number=42): 这样的做法
2 、 __getitem__(self, key) 定义的是 self[key] 的行为。
想要能被迭代,需要定义 __iter__(self)
3 、 __add__ 是改变 + 或者说 operator.add 的实现
你写下了 a + b,实际上 Python 做了这样的工作:
a 是否定义了 __add__,如是,则调用 a.__add__(b),如果没有引发异常或者返回 NotImplemented 则 a+b 的结果就是其返回值
否则检查 b.__radd__,剩下同上
如果仍然不成功,则引发 TypeError 。

SymPy 有大用。
pigspy
pigspy
2020-08-28 14:49:22 +08:00
比如说一个二维向量
class Vec(object):
x:int
y:int

那么要表示两个向量相加,就可以使用运算符重载为

def __add__(self, other:Vec):
this.x -= other.x
this.y -= other.y
Trim21
Trim21
2020-08-28 14:50:51 +08:00
好像只定义__len__和 get item 也能迭代,之前看过一眼有这种说法,说错了不要打我()
InkStone
InkStone
2020-08-28 14:50:56 +08:00
1. 函数声明有规则,这是跟 Python 的约定,一定要这么写。不然运行时 Python 调用时会报错。每种运算符都不一样。但具体实现无所谓,甚至不一定要实现+这个功能。

2. 为了让自定义类更有扩展性,用起来方便。迭代和下标引用不是一回事。迭代实现__iter__,下标引用实现__iter__。pass 不行,必须得实现。
3.不对。python 没有为非基础类型封装+这个操作,更不要说你自己的类型。 最后一个问题的回答同问题二。
no1xsyzy
no1xsyzy
2020-08-28 14:53:07 +08:00
就历史上来说,早期的 Python 非常的多的值都不是对象。
比如 tuple 之前就不是对象,所以无法运用对象语法 tup.len ,所以构成了 len(tup) 的做法。
然而后来发现要自己 OO 的话,必须要能够重载这些 builtin 函数,包括 len() str() repr() iter() 等
所以采用 “给类定义魔法方法” 的方式来改变 builtin 函数的行为。

所谓 “魔法”,在 Python 里就是说 “一般不需要使用,该使用的时候你自会知道该使用”
比如 metaclass
知道下它影响了什么就好了。
xiri
xiri
2020-08-28 14:53:17 +08:00
1. 不一定,没有固定的规则,这个是随你自己定的,但是你这样定义了之后使用改运算符的时候就要满足你自己的要求
2. 定义 __getitem__ 是为了能用索引访问元素啊(通过类似 p[i]这种形式取值),拥有迭代功能只是附加的
3. 重载“+”号你就能用该符号做计算,里面具体怎么算是没有要求的,你完全可以重载“+”号,然后把它的功能写成相减、相乘都可以

用处( v2 的回复不支持 md,并且会丢失缩进,将就看吧):
比如你定义了一个类用来表述复数:

class ComplexNum:
def __init__(self, a,b):
self.a = a
self.b = b


这时候考虑两个复数间的运算(以相加为例):

x=ComplexNum(1,2) //表示复数 1+2i
y=ComplexNum(2,3) //复数 2+3i
x+y?

由于这个类是你自己定义的,python 不知道相加的时候该怎么处理,你这时候直接加就会报错
你完全可以单独取出每个对象中的 a 、b 值自己计算,但更好的办法是重载“+”运算符:

class ComplexNum:
def __init__(self, a,b):
self.a = a
self.b = b
def __add__(self, other):
return ComplexNum(self.a+other.a , self.b+other.b)

这样就可以直接使用“+”运算符计算了

z=x+y
print(z.a) //3
print(z.b) //5
ipwx
2020-08-28 14:53:25 +08:00
楼主是不是从 C++ 过来的。。

pass 不是 = default,而是“啥也不干的占位符”。
xiaolinjia
2020-08-28 14:54:58 +08:00
问题 1: 有一定的规则,至少接收 2 个参数。一个表示 + 左边的对象,一个表示 + 右边的对象。
当然你 __add__ 方法签名里也可以加多个参数,不过跟我们一般的期望不符合。也不一定是 return self.val + other,只所以 return 这个,是因为我们想得到他们的和的结果。
问题 2:拥有迭代功能的背后是这个类被 iter 调用后可以返回一个迭代器。只要实现了 __iter__ 就可以,如果没 __iter__,可以退一步实现 __getitem__,也可以迭代。这时,会从 __getitem__(0) 开始迭代。
问题 3:+ 号对应每个类型的 __add__ 方法,比如 int 类型,他是 py 已经定义了 __add__ 方法,那他就可以 + 。像这例子的话,如果 self.val 传入了一个自定义的类型,且你这个类型没有定义 __add__ 方法。

你可以跑这个看看。
class Test:
def __init__(self, val):
self.val = val

def __add__(self, other, c=1):
print(1 + c)

def __getitem__(self, item):
pass


if __name__ == '__main__':
t = Test(1)
t1 = Test(2)
t + t1
for i in t:
print(i)
uswood
2020-08-28 15:17:11 +08:00
我的天,别的论坛都死气沉沉的,没想到收到这么多回复~ 谢谢大神 我先看看 有问题再提 @no1xsyzy @pigspy @InkStone @Trim21 @ipwx @xiaolinjia @xiri @
uswood
2020-08-28 15:20:02 +08:00
@ipwx 哈哈 没有没有 我就是偷懒不想写函数了
imn1
2020-08-28 15:27:01 +08:00
list_a * list_b
我自定义了这个,实际就是 itertools.product
不过不敢动 buildin,是另设一个类
同样我还定义了 str_a - str_b,实际就是 str_a.replace(str_b, '')

不过不好玩,也很少用
……
uswood
2020-08-28 15:29:05 +08:00
@xiri @xiaolinjia @no1xsyzy @pigspy @InkStone 明白了很形象,深入一点说,是不是意味着 1 、形参是什么随便定,只满足内部自己编写的处理流程就行,比如我要定义+,就需要 2 个数字 /字符串,我要切片,就要在__getitem__中调用 slice 内置函数-------所以运算符重载只是给自己的类添加处理模式,而不是重新定义“+”这个运算符的意义,只不过你在自己类的实例上写“+”的时候,python 会调用你的方法去处理而已~

误会了,还以为是自己要怎么去定义 python 的加法、索引。。
no1xsyzy
2020-08-28 15:33:00 +08:00
@uswood #9 其实是因为你的问题太简单大家都能回答(
#12 对,其实就是一种 “约定 > 配置”。
uswood
2020-08-28 15:33:43 +08:00
@uswood 不过之所以这么误会是因为类的继承关系,以为在自己的类中写上__add__等于覆盖了 python 内置的类方法,但是没想到自己定义的类压根就不是内置类型的子类,压根不会去继承。。
princelai
2020-08-28 16:40:40 +08:00
所有原始类型都继承自 object,你可以看看 dir(object)
里面根本没有__add__,只不过恰好“+”被解释为__add__,而加法是一个双目中缀符号,必须有两个参数

例如我定义一个类并实例化
```
class Test:
def __init__(self, a):
self.a = a
def __add__(self, other):
return self.a + other

t1 = Test(2)
```
那么下面三种是一个意思
```
t1+5
t1.__add__(5)
Test.__add__(t1,5)
```
+号只是解释器帮你解释为__add__,其他的方法除了第一种没办法实现,后两种都是可以的
Ricardoo
2020-08-28 17:44:18 +08:00
这个时候我就安利一本书了--《流畅的 python 》,查看第十一章 接口:从协议到抽象基类和第十三章 正确重载运算符
zhaofq
2020-08-28 18:59:26 +08:00
@Ricardoo 同样安利,这个非常适合你现在的情况
volvo007
2020-08-28 19:04:29 +08:00
@Ricardoo 我很怀疑 LZ 就是这本书没看懂才来问的😂
uswood
2020-08-28 20:59:10 +08:00
@princelai 明白了谢谢~ 我看的这本书翻译非常晦涩,还以为它是说替代了系统的加法运算
uswood
2020-08-28 21:00:19 +08:00
@Ricardoo 哈哈哈 我竟然发现书架上有 谢谢安利 ,等学深入了再去看,感觉学完基础先学着做点实际的会比较维持兴趣

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/702208

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX