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

增强型算术赋值详解:“-=”操作是如何实现的?

时间:2023-03-19 13:29:38 科技观察

前言本文为Python语法糖系列之一。最新的源代码可以在desugar项目(https://github.com/brettcannon/desugar)中找到。介绍Python有一种叫做增强算术赋值的东西。你可能不熟悉这个术语,但它实际上是在进行数学运算时的赋值,例如a-=b是减法的增强算术赋值。增强赋值是在Python2.0版中添加的。(译注:在PEP-203中引入)剖析-=由于Python不允许重写赋值,因此与其他具有特殊/魔术方法的操作相比,它可能无法完全按照您的想象实现扩充赋值。首先,知道a-=b在语义上与a=a-b相同。但也要意识到,如果你事先知道你要将一个对象赋值给一个变量名,那么它可能比盲目操作a-b效率更高。例如,最小的好处是避免创建一个新对象:如果一个对象可以就地修改,那么返回self比构造一个新对象更有效。因此,Python提供了__isub__()方法。如果它在赋值的左侧定义(通常称为左值),则调用右侧的值(通常称为右值)。所以对于a-=b,它会尝试调用a.__isub__(b)。如果调用的结果是NotImplemented,或者根本没有结果,那么Python会回退到常规的二进制算术:a-b。(译注:作者关于二元运算的文章,译文在此)最后无论使用哪种方法,返回值都会赋值给a。下面是一个简单的伪代码,a-=b分解为:#realizea-=b伪代码ifhasattr(a,"__isub__"):_value=a.__isub__(b)if_valueisnotImplemented:a=_valueelse:a=a-bdel_valueelse:a=a-b概括了这些方法因为我们已经实现了二进制算术,所以推广增广算术并不太复杂。通过传入二进制算术运算函数,并进行一些自省(如果发生类型错误则进行处理),可以很好地总结为:def_create_binary_inplace_op(binary_op:_BinaryOp)->Callable[[Any,Any],Any]:binary_operation_name=binary_op.__name__[2:-2]method_name=f"__i{binary_operation_name}__"operator=f"{binary_op._operator}="defbinary_inplace_op(lvalue:Any,rvalue:Any,/)->Any:lvalue_type=type(lvalue)try:method=debuiltins._mro_getattr(lvalue_type,method_name)exceptAttributeError:passelse:value=method(lvalue,rvalue)ifvalueisnotNotImplemented:returnvaluetry:returnbinary_op(lvalue,rvalue)exceptTypeErrorasexc:#IftheTypeErrorisduetothebinaryarithmeticooperator,suppress#itsowecanraisetheappropriateonefortheagumented_op!binary_exsignment.if值=二进制操作。“实施Augmentedarithmeticassignment`a{operator}b`.""")returnbinary_inplace_op这使得定义的-=支持_create_binary_inplace_op(__sub__),并且可以推断出其他的东西:函数名,要调用什么__i*__函数,以及什么时候二进制可调用到算术出错时调用?我发现几乎没有人使用**=在为本文编写代码时,我遇到了一个带有**=的奇怪测试错误。始终确保__pow__是正确的测试之一从地面调用的Python标准库中的操作模块失败。我的代码通常很好,我的代码和CPython的代码之间的差异通常意味着我有问题。但是,无论我多么仔细代码,我找不到为什么我的测试通过而标准库失败的原因。我决定更深入地研究CPython内部发生的事情。从反汇编的字节码开始:>>>deftest():a**=b...>>>importdis>>>dis.dis(test)10LOAD_FAST0(a)2LOAD_GLOBAL0(b)4INPLACE_POWER6STORE_FAST0(a)8LOAD_CONST0(None)10RETURN_VALUE通过它,我在eval循环中找到了INPLACE_POWER:;Py_DECREF(exp);SET_TOP(res);if(res==NULL)gotoerror;DISPATCH();}来源:https://github.com/python/cpython/blob/v3.8.3/Python/ceval.c#L1677然后找到PyNumber_InPlacePower():PyObject*PyNumber_InPlacePower(PyObject*v,PyObject*w,PyObject*z){if(v->ob_type->tp_as_number&&v->ob_type->tp_as_number->nb_inplace_power!=NULL){returnternary_op(v,w,z,NB_SLOT(nb_inplace_power),"**=");}else{returnternary_op(v,w,z,NB_SLOT(nb_power),"**=");}}来源:https://github..._,__pow__将被调用。但是,正确的做法是调用__pow__和__rpow__如果调用__ipow__出现问题,返回NotImplemented或根本不返回。换句话说,当__ipow__存在时,上面的代码不小心跳过了a**b的回退语义!事实上,大约11个月前,这个问题已部分被发现并提交了一个错误。我修复了它并在python-dev上做了一个注释。截至目前,这看起来将在Python3.10中得到修复,我们还需要添加一个通知,即**=在3.8和3.9的文档中存在错误(这个问题可能很久以前就存在,但旧的Python版本已经处于唯一的安全维护模式,因此文档不会更改)。固定代码很可能不会被移植,因为它是语义更改,很难判断是否有人不小心依赖了相关语义。但是这个问题拖了这么久才被发现,这说明**=并没有被广泛使用,否则问题早就被发现了。原标题|解开增广算术赋值BrettCannon转载本文请联系Python猫公众号。