点此添加图片标题原标题|解开Python中的二进制算术运算BrettCannon翻译|豌豆花下的猫来源|Pythoncat语句|本翻译仅供交流学习,基于CCBY-NC-SA4.0许可协议。为便于阅读,内容略有修改。对我解释属性访问的博客文章的压倒性回应启发我写了另一篇关于Python语法中有多少实际上只是语法糖的文章。在本文中,我想谈谈二进制算术运算。具体来说,我想破译减法的工作原理:a-b。我故意选择减法,因为它不可交换。这可以强调操作顺序的重要性,而不是错误地执行a和b而仍然得到相同结果的加法。查看C代码按照惯例,我们首先查看CPython解释器编译的字节码。defsub():a-b...importdisdis.dis(sub)10LOAD_GLOBAL0(a)2LOAD_GLOBAL1(b)4BINARY_SUBTRACT6POP_TOP8LOAD_CONST0(None)10RETURN_VALUE看起来我们需要深入研究BINARY_SUBTRACT操作码。翻看Python/ceval.c文件,可以看到实现这个操作码的C代码如下:diff=PyNumber_Subtract(left,right);Py_DECREF(right);Py_DECREF(left);SET_TOP(diff);if(diff==NULL)gotoerror;DISPATCH();}来源:https://github.com/python/cpy...这里的关键代码是PyNumber_Subtract(),它实现了减法的实际语义。继续看这个函数的一些宏,可以找到binary_op1()函数。它提供了一种管理二进制操作的通用方法。但是,我们并没有将其作为实现的参考,而是使用了Python数据模型。官方文档非常好,清楚地介绍了减法使用的语义。从数据模型中学习通读数据模型的文档,你会发现在实现减法时,有两个方法起着关键作用:__sub__和__rsub__。1、__sub__()方法执行a-b时,会在a的类型中寻找__sub__(),然后将b作为其参数。这很像我关于属性访问的文章中的__getattribute__(),特殊/魔术方法是根据对象的类型解析的,而不是出于性能目的的对象本身;在下面的示例代码中,我使用_mro_getattr()指示此过程。因此,如果定义了__sub__(),则type(a).__sub__(a,b)将用于减法运算。(译注:魔术方法属于对象的类型,不属于对象)这意味着本质上,减法只是一个方法调用!你也可以理解为标准库中的operator.sub()函数。我们将模拟此函数来实现我们自己的模型,使用名称lhs和rhs分别表示a-b的左侧和右侧,以使示例代码更易于理解。通过调用__sub__()实现减法defsub(lhs:Any,rhs:Any,/)->Any:"""实现二元运算a-b."""lhs_type=type(lhs)try:subtract=_mro_getattr(lhs_type,"__sub__")exceptAttributeError:msg=f"unsupportedoperandtype(s)for-:{lhs_type!r}和{type(rhs)!r}"raiseTypeError(msg)else:returnsubtract(lhs,rhs)2.让右边使用__rsub__()但是如果a没有实现__sub__()呢?如果a和b是不同的类型,那么我们尝试在b上调用rsub__()(__rsub中的“r”表示“右”,表示在运算符的右侧)。当操作的两侧是不同类型时,这确保了它们都有机会尝试使表达式起作用。当它们相同时,我们假设__sub__()将处理它。但是,即使双方具有相同的实现,您仍然必须调用__rsub__()以防其中一个对象属于另一个(子)类。3.不用关心类型现在,表达式的两边都可以参与运算了!但是,如果由于某种原因,一个对象的类型不支持减法(例如4-“stuff”)怎么办?在这种情况下,__sub__或__rsub__所能做的就是返回NotImplemented。这是Python返回的信号,它应该继续下一件事,试图让代码工作。对于我们的代码,这意味着在假设方法有效之前检查方法的返回值。减法的实现,表达式左右两边都可以参与运算:lhs_method=debuiltins._mro_getattr(lhs_type,"__sub__")exceptAttributeError:lhs_method=_MISSINGlhs.__rsub__(了解是否应该先调用rhs.__rub__)try:lhs_rmethod=debuiltins._mro_getattr(lhs_type,"__rsub__")exceptAttributeErrorlhs_rmethod=_MISSINGrhs.__rsub__rhs_type=type(rhs)try:rhs_method=debuiltins._mro_getattr(rhs_type,"__rsub__")除了AttributeError:rhs_method=_MISSINGcall_lhs=lhs,lhs_method,rhscall_rhs=rhs,rhs_method,lhsifnot=lhs_typeiscalls_type:,call_rhselse:calls=(call_lhs,)forfirst_obj,meth,second_objincalls:ifmethis_MISSING:continuevalue=meth(first_obj,second_obj)ifvalueisnotImplemented:returnvalueelse:raiseTypeError(f"unsupportedoperandtype(s))对于-:{lhs_type!r}和{rhs_type!r}")4.S子类优先于父类如果您查看__rsub__()的文档,您会注意到一条注释。它说如果一个减法表达式右边是左边的子类(真正的子类,同一个类不算),并且两个对象的__rsub__()方法不同,那么__sub__()就会被调用在先调用__rsub__()之前。换句话说,如果b是a的子类,则调用顺序相反。这似乎是一个奇怪的特例,但背后是有原因的。当你创建一个子类时,就意味着你在父类提供的操作上注入了新的逻辑。这种逻辑不一定要加在父类中,否则父类在对子类进行操作时,很容易重写子类想要实现的操作。具体来说,假设您有一个名为Spam的类,当您执行Spam()-Spam()时,您将获得LessSpam的一个实例。然后你创建一个名为Bacon的Spam子类,这样当你从Spam中减去Bacon时,你会得到VeggieSpam。如果没有上述规则,Spam()-Bacon()将导致LessSpam,因为Spam不知道减去Bacon会导致VeggieSpam。但是,按照上面的规则,会得到预期的结果VeggieSpam,因为表达式中会先调用Bacon.__rsub__()(如果计算是Bacon()-Spam(),那么就会得到正确的结果,因为先调用bacon.__sub__(),规则上说需要区分两个类的不同方法,而不仅仅是issubclass()判断的子类。)Python中减法的完整实现_MISSING=object()defsub(lhs:Any,rhs:Any,/)->Any:lhs.__sub__lhs_type=type(lhs)try:lhs_method=debuiltins._mro_getattr(lhs_type,"__sub__")exceptAttributeError:lhs_method=_MISSINGlhs.__rsub__(为了知道是否应该先调用rhs.__rub__):rhs_method=debuiltins._mro_getattr(rhs_type,"__rsub__")exceptAttributeError:rhs_method=_MISSINGcall_lhs=lhs,lhs_method,rhscall_rhs=rhs,rhs_method,lhsif(rhs_typeisnot_MISSING#Dowecare?rhs_typeisnotlhs_type#RHS可以是asubclass?andissubclass(rhs_type,lhs_type)#RHSisasubclass!andlhs_rmethodisnotrhs_method#r*实际上是不同的吗?):calls=call_rhs,call_lhseliflhs_typeisnotrhs_type:calls=call_lhs,call_rhselse:calls=(call_lhs,)forfirst_obj,meth,second_objincalls:ifmethis_MISSING:continuevalue=meth(first_obj,second_obj)ifvalueisnotNotImplemented:returnvalueelse:raiseTypeError(f"unsupportedoperandtype(s)for-:{lhs_type!r}and{rhs_type!r}")到其他两个元运算解决了减法运算,那么其他二元运算呢?好吧,事实证明它们的操作相同,只是碰巧使用了不同的特殊/魔术方法名称所以,如果我们可以概括这种方法,那么我们可以实现13个操作的语义:+、-、、@、/、//、%、*、<<、>>、&、^和|。由于闭包和Python在对象自省方面的灵活性,我们可以抽象出运算符函数的创建。一个创建闭包的函数,实际发现了二元运算的递辑_MISSING=object()def_create_binary_op(name:str,operator:str)->Any:"""创建二元运算函数。name参数指定名称用于二元运算的特殊方法的名称(例如__sub__的sub)。运算符名称是表示二元运算的标记(例如-用于减法)。"""lhs_method_name=f"__{name}__"defbinary_op(lhs:Any,rhs:Any,/)->Any:"""在Python中实现二进制操作的闭包。"""rhs_method_name=f"__r{name}__"lhs.__*__lhs_type=type(lhs)try:lhs_method=debuiltins._mro_getattr(lhs_type,lhs_method_name)exceptAttributeError:lhs_method=_MISSINGlhs.__r__(为了知道是否应该先调用rhs.__r__)try:lhs_rmethod=debuiltins._mro_getattr(lhs_type,rhs_method_name)exceptAttributeError:lhs_rmethod=_MISSING__rs_type*rhs.__r=类型(rhs)尝试:rhs_method=debuiltins._mro_getattr(rhs_type,rhs_method_name)除了AttributeError:rhs_method=_MISSINGcall_lhs=lhs,lhs_method,rhscall_rhs=rhs,rhs_method,lhsif(rhs_typeisnot_MISSING#Dowecare?andrhs_typeisnotlhs_type#RHScanbeasubclass?andissubclass(rhs_type,lhs_type)#RHSisasubclass!和lods_isnotrhs_method#r*实际上不同吗?):calls=call_rhs,call_lhseliflhs_typeisnotrhs_type:calls=call_lhs,call_rhselse:calls=(call_lhs,)forfirst_obj,meth,second_objincalls:ifmethistinueconuevalue=MISSING:meth(first_obj,second_obj)ifvalueisnotImplemented:returnvalueelse:exc=TypeError(f"{operator}不支持的操作数类型:{lhs_type!r}和{rhs_type!r}")exc._binary_op=operatorraiseexc通过这段代码,可以将减法运算定义为_create_binary_op(“sub”,“-”),然后根据需要重复定义其他运算。更多信息可以在本博客的“语法糖”选项卡中找到更多详细解释Python语法的文章。更正2020-08-19:修复了在__sub__()之前调用__rsub__()时的规则。2020-08-22:修复了类型相同时不调用__rsub__的问题;还压缩了转换代码,只保留打开和关闭代码,这让我的生活更轻松。2020-08-23:为大多数示例添加了内容
