当前位置: 首页 > 后端技术 > Python

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

时间:2023-03-26 13:48:10 Python

看C代码按照惯例,我们先看CPython解释器编译的字节码。>>>defsub():a-b...>>>importdis>>>dis.dis(sub)10LOAD_GLOBAL0(a)2LOAD_GLOBAL1(b)4BINARY_SUBTRACT6POP_TOP8LOAD_CONST0(无)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==NULL)转到错误;DISPATCH();}这里的关键代码是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__")除了AttributeError: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)尝试:lhs_method=debuiltins._mro_getattr(lhs_type,"__sub__")除了AttributeError:lhs_method=_MISSING#lhs.__rsub__(为了知道是否应该首先调用rhs.__rub__)尝试:lhs_rmethod=debuiltins._mro_getattr(lhs_type,"__rsub__")除了属性错误:lhs_rmethod=_MISSING#rhs.__rsub__rhs_type=type(rhs)lhs如果lhs_type不是rhs_type:calls=call_lhs,call_rhselse:calls=(call_lhs,)forfirst_obj,meth,second_obj在调用中:如果meth是_MISSING:继续value=meth(first_obj,second_obj)如果value不是NotImplemented:返回值else:raiseTypeError(f"unsupportedoperandtype(s)for-:{lhs_type!r}and{rhs_type!r}")4.Subclassoversuperclass,同一个类不算),并且两个对象的__rsub__()方法不同,则__rsub__()会先调用__sub__()。换句话说,如果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__(为了知道是否应该首先调用rhs.__rub__)尝试:lhs_rmethod=debuiltins._mro_getattr(lhs_type,"__rsub__")exceptAttributeError:lhs_rmethod=_MISSING#rhs.__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,lhsif(rhs_typeisnot_MISSING#Dowecare?andrhs_typeisnotlhs_type#RHS可以是子类吗?andissubclass(rhs_type,lhs_type)#RHS是一个子类!andlhs_rmethodisnotrhs_method#__r*__实际上是不同的吗?):calls=call_rhs,call_lhseliflhs_typeisnotrhs_type:calls=call_lhs,call_rhselse:calls=(call_lhst_ob,)for,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}")tootherbinaryoperations解决减法运算,那么其他二元运算呢?好吧,事实证明它们的操作相同,只是碰巧使用了不同的特殊/魔术方法名称所以,如果我们可以概括这种方法,那么我们可以实现13个操作的语义:+、-、、@、/、//、%、*、<<、>>、&、^和|。由于闭包和Python在对象自省方面的灵活性,我们可以抽象出运算符函数的创建。#一个创建封闭包的函数,实际发现了二元运算的递辑_MISSING=object()def_create_binary_op(name:str,operator:str)->Any:"""创建二元运算函数。`name`参数指定用于二元运算的特殊方法的名称(例如`sub`表示__sub__`)。`operator`名称是表示二元运算的标记(例如`-`表示减法)。"""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*__(知道是否应该先调用rhs.__r*__)试试:lhs_rmethod=debuiltins._mro_getattr(lhs_type,rhs_method_name)除了AttributeError:lhs_rmethod=_MISSING#rhs.__r*__rhs_type=type(rhs)try:rhs_method=debuiltins._mro_getattr(rhs_type,rhs_method_name)exceptAttributeError:rhs_method=_MISSINGcall_lhs=lhs,lhs_methodcall_rhs=rhs,rhs_method,lhsif(rhs_typeisnot_MISSING#Dowecare?andrhs_typeisnotlhs_type#RHScanbeasubclass?andissubclass(rhs_type,lhs_type)#RHS是一个子类!lhs_rmethod不是rhs_method#Is__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(第一的_obj,second_obj)如果值未实现:返回值else:exc=TypeError(f"{operator}不支持的操作数类型:{lhs_type!r}和{rhs_type!r}")exc._binary_op=operatorraiseexc有了这段代码,就可以定义减法运算为_create_binary_op("sub","-"),然后根据需要重复定义其他运算欢迎来到公众号:Python编程学习圈,发“J”获取免费,每日干货分享

最新推荐
猜你喜欢