花猫语:本译本依旧是Brett的《Python语法糖》系列。当他演示“-=”操作的实现时,他发现了CPython中的一个错误并轻松修复了它。是个大家伙……原标题|解开增广算术赋值作者|BrettCannon译者|猫下豌豆花(《蟒猫》公众号作者)声明|本翻译仅供交流学习,基于CCBY-NC-SA4.0许可协议。为便于阅读,内容略有修改。前言本文是Python语法糖系列之一。最新的源代码可以在desugar项目(https://github.com/brettcanno...)中找到。介绍Python有一种叫做增强算术赋值的东西。你可能不熟悉这个术语,但它实际上是在进行数学运算时的赋值,例如a-=b是减法的增强算术赋值。增强赋值是在Python2.0版中添加的。(注解:在PEP-203中引入)Profiling-=因为Python不允许覆盖赋值,与其他具有特殊/魔术方法的操作相比,它可能会以与您想象的不同的方式实现扩充赋值。首先,知道a-=b在语义上与a=a-b相同。但也要意识到,如果你事先知道你要将一个对象赋值给一个变量名,那么它可能比盲目操作a-b效率更高。例如,最小的好处是避免创建一个新对象:如果一个对象可以就地修改,那么返回self比构造一个新对象更有效。因此,Python提供了__isub__()方法。如果它定义在赋值的左侧(通常称为lvalue#lrvalue),则调用右侧的值(通常称为rvalue)。所以对于a-=b,它会尝试调用a.__isub__(b)。如果调用的结果是NotImplemented,或者根本没有结果,那么Python会回退到常规的二进制算术:a-b。(译注:作者关于二元运算的文章,译文在此)最后无论使用哪种方法,返回值都会赋值给a。下面是一个简单的伪代码,a-=b分解为:#实现a-=b的伪代码ifhasattr(a,"__isub__"):_value=a.__isub__(b)if_valueisnotNotImplemented: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)除了AttributeError:passelse:value=method(lvalue,rvalue)ifvalueisnotNotImplemented:returnvaluetry:returnbinary_op(lvalue,rvalue)exceptTypeErrorasexc:#如果TypeError是由于二元算术运算符引起的,则抑制它#以便我们可以为约定的赋值提出适当的错误。如果exc._binary_op!=binary_op._operator:提高raiseTypeError(f"{operator}不支持的操作数类型:{lvalue_type!r}和{type(rvalue)!r}")binary_inplace_op.__name__=binary_inplace_op.__qualname__=method_namebinary_inplace_op.__doc__=(f"""实现增广算术赋值`a{operator}b`.""")returnbinary_inplace_op这使得定义的-=支持_create_binary_inplace_op(__sub__),并且可以推断出其他内容:函数名,调用什么__i*__函数,以及哪个可调用当二进制算法出现问题时调用我发现几乎没有人使用**=在为本文编写代码时,我遇到了一个奇怪的测试错误**=。在确保__pow__被正确调用的所有测试中,Python标准库中有一个针对operator模块的测试用例失败了。我的代码通常没有问题,如果我的代码和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:caseTARGET(INPLACE_POWER):{PyObject*exp=POP();PyObject*base=TOP();PyObject*res=PyNumber_InPlacePower(base,exp,Py_None);Py_DECREF(基础);Py_DECREF(exp);SET_TOP(资源);如果(res==NULL)转到错误;派遣();}来源:https://github.com/python/cpy...然后找到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.com/python/cpy...松一口气~代码显示如果定义了__ipow__就会调用,但是__pow__只有在没有__ipow__的时候才会调用但是,正确的做法应该是:如果调用__ipow__有问题,返回NotImplemented或者根本不返回,那么应该调用__pow__和__rpow__。换句话说,当__ipow__存在时,上面的代码不小心跳过了a**b的回退语义!事实上,大约11个月前,这个问题被部分发现并提交了一个错误。我修复了它并在python-dev上做了一个注释。截至目前,这看起来将在Python3.10中得到修复,我们还需要添加一条通知,指出**=在3.8和3.9的文档中存在错误(该问题可能在更早的时候就存在,但旧的Python版本已经在安全维护模式,因此文档不会更改)。固定代码很可能不会被移植,因为它是语义更改,很难判断是否有人不小心依赖了相关语义。但是这个问题拖了这么久才被发现,这说明**=并没有被广泛使用,否则问题早就被发现了。
