当前位置: 首页 > 科技观察

详解Python的二进制算术运算,为什么减法只是语法糖?

时间:2023-03-21 21:20:11 科技观察

对我解释属性访问的博客文章的压倒性回应激发了我写另一篇关于Python的语法中有多少实际上只是语法糖的文章。在本文中,我想谈谈二进制算术运算。具体来说,我想破译减法的工作原理:a-b。我故意选择减法,因为它不可交换。这可以强调操作顺序的重要性,而不是错误地执行a和b而仍然得到相同结果的加法。查看C代码按照惯例,我们首先查看CPython解释器编译的字节码。>>>defsub():a-b...>>>importdis>>>dis.dis(sub)10LOAD_GLOBAL0(a)2LOAD_GLOBAL1(b)4BINARY_SUBTRACT6POP_TOP8LOAD_CONST0(None)10RETURN_VALUE看起来我们需要深入研究BINARY_SUBTRACT操作码。翻看Python/ceval.c文件,可以看到实现这个操作码的C代码如下:caseTARGET(BINARY_SUBTRACT):{PyObject*right=POP();PyObject*left=TOP();PyObject*diff=PyNumber_Subtract(左,右);Py_DECREF(右);Py_DECREF(左);SET_TOP(diff);if(diff==NULL)gotoerror;DISPATCH();}来源:https://github.com/python/cpython/blob/6f8c8320e9eac9bc7a7f653b43506e75916ce8e8/Python/ceval.c#L1569-L1579这里的关键代码是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}and{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返回的信号,它应该继续下一件事,试图让代码工作。对于我们的代码,这意味着在假设方法有效之前检查方法的返回值。#减法的实现,表达式左右两边都可以参与运算_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=_MISSING#lhs.__rsub__(forknowingifrhs.__rub__shouldbecalledfirst)try:lhs_rmethod=debuiltins._mro_getattr(lhs_type,"__rsub__")exceptAttributeError:lhs_rmethod=_MISSING.__rsub__(forknowingifrhs.__rub__shouldbecalledfirst)尝试:lhs_rmethod=debuiltins._mro_getattr(lhs_type,"__rsub__")exceptAttributeError:lhs_rmethod=_MISSING#subrhs类型(rhs)尝试:rhs_method=debuiltins._mro_getattr(rhs_type,“__rsub__”)exceptAttributeError:rhs_method=_MISSINGcall_lhs=lhs,lhs_method,rhscall_rhs=rhs,rhs_method,lhsiflhs_typeisnotrhs_type:calls=call_lhs,call_rhsj,(else:calls)meth,second_objincalls:ifmethis_MISSING:continuevalue=meth(first_obj,second_obj)ifvalueisnotImplemented:returnvalueelse:raiseTypeError(f"unsupportedoperandtype(s)for-:{lhs_type!r}and{rhs_type!r}")4.子类优先于父类如果您查看_的文档_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=_MISSING#lhs.__rsub__(forknowingifrhs.__rub__shouldbecalledfirst)try:lhs_rmethod=debuiltins._mro_getattr(lhs_type,"__rsub__")exceptAttributeError:lhs_rmethod=_MISSING#rhs.__rsub__rhs_type=type(rhs)try=rhins_._mro_getattr(rhs_type,"__rsub__")exceptAttributeError:rhs_method=_MISSINGcall_lhs=lhs,lhs_method,rhscall_rhs=rhs,rhs_method,lhsif(rhs_typeisnot_MISSING#Dowecare?andrhs_typeisnotlhs_type#CouldRHSbeasubclass?andissubclass(rhs_type,lhs_type)#RHSisasubclass!andlhs_rmethodisnotrhs_method#Is__r*__actuallydifferent?):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)ifvalueisnotImplemented: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`for`__sub__`).The`operator`nameisthetokenrepresentingthebinaryoperation(e.g.-`forsubtraction)."""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=_MISSING#lhs.__r*__(forknowingifrhs.__r*__shouldbecalledfirst)try:lhs_rmethod=debuiltins._mro_getattr(lhs_type,rhs_method_name)exceptAttributeError:lhs_rmethod=_MISSING#rhs.__r*__rhs_type=type(rhs)try:rhs_method=debuiltins._mro_getattr(rhs_type,rhsnameexceptAttribute_Error)=_管理信息系统SINGcall_lhs=lhs,lhs_method,rhscall_rhs=rhs,rhs_method,lhsif(rhs_typeisnot_MISSING#Dowecare?andrhs_typeisnotlhs_type#CouldRHSbeasubclass?andissubclass(rhs_type,lhs_type)#RHSisasubclass!andlhs_rmethodisnotrhs_method#Is__r*__actuallydifferent?):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:exc=TypeError(f"unsupportedoperandtype(s)for{operator}:{lhs_type!r}and{rhs_type!r}")exc._binary_op=operatorraiseexc通过这段代码,您可以将减法运算定义为_create_binary_op("sub","-"),然后根据需要重复定义其他运算。通过此博客获得更多信息在“SyntaxSugar”选项卡中,您可以找到更多详细解释Python语法的文章。可以在https://github.com/brettcannon/desugar找到源代码。更正2020-08-19:修复了在__sub__()之前调用__rsub__()时的规则。2020-08-22:修复了当类型相同时不调用__rsub__的问题;还剥离了转换代码,只保留打开和关闭代码,这让我的生活更轻松。2020-08-23:为大多数示例添加了内容。原标题|解开Python中的二进制算术运算BrettCannon翻译|猫下豌豆花(《蟒猫》公众号作者)声明|本翻译以交流学习为目的,基于CCBY-NC-SA4.0许可协议。为便于阅读,内容略有修改。本文转载自微信公众号“蟒猫”,可通过以下二维码关注。转载本文请联系Python猫公众号。