Home > Article > Backend Development > Detailed explanation of binary arithmetic operations in Python
Related learning recommendations: python tutorial
Everyone responded enthusiastically to my blog post about interpreting attribute access, which inspired me I'll write another article about how much of Python's syntax is actually just syntactic sugar. In this article I want to talk about binary arithmetic operations.
Specifically, I want to explain how subtraction works: a - b
. I chose subtraction on purpose because it is not commutative. This emphasizes the importance of the order of operations, as compared to the addition operation, where you can mistakenly flip a and b during implementation and still get the same result.
As usual, we start by looking at the bytecode compiled by the CPython interpreter.
>>> def sub(): a - b... >>> import dis>>> dis.dis(sub) 1 0 LOAD_GLOBAL 0 (a) 2 LOAD_GLOBAL 1 (b) 4 BINARY_SUBTRACT 6 POP_TOP 8 LOAD_CONST 0 (None) 10 RETURN_VALUE复制代码
Looks like we need to dig into the BINARY_SUBTRACT opcode. Checking the Python/ceval.c file, you can see that the C code that implements this opcode is as follows:
case TARGET(BINARY_SUBTRACT): { PyObject *right = POP(); PyObject *left = TOP(); PyObject *diff = PyNumber_Subtract(left, right); Py_DECREF(right); Py_DECREF(left); SET_TOP(diff); if (diff == NULL) goto error; DISPATCH(); }复制代码
Source: github.com/python/cpyt…
The key code here is PyNumber_Subtract (), implements the actual semantics of subtraction. Continuing to look at some of the macros for this function, you can find the binary_op1() function. It provides a general way to manage binary operations.
However, we do not use it as a reference for implementation, but use the Python data model. The official documentation is very good and clearly introduces the semantics used in subtraction.
Read through the documentation of the data model, and you will find that there are two methods that play a key role in implementing subtraction: __sub__ and __rsub__.
When a - b
is executed, __sub__() will be found in the type of a, and then b will be used as its parameter. This is much like __getattribute__() in my article about attribute access, the special/magic methods are parsed based on the type of the object, not the object itself for performance purposes; in the example code below, I use _ mro_getattr() represents this process.
Therefore, if __sub__() is defined, type(a).__sub__(a,b) will be used for subtraction. (Annotation: Magic methods belong to the type of the object, not the object)
This means that in essence, subtraction is just a method call! You can also think of it as the operator.sub() function in the standard library.
We will imitate this function to implement our own model, using the two names lhs and rhs, representing the left and right sides of a-b respectively, to make the sample code easier to understand.
# 通过调用__sub__()实现减法 def sub(lhs: Any, rhs: Any, /) -> Any: """Implement the binary operation `a - b`.""" lhs_type = type(lhs) try: subtract = _mro_getattr(lhs_type, "__sub__") except AttributeError: msg = f"unsupported operand type(s) for -: {lhs_type!r} and {type(rhs)!r}" raise TypeError(msg) else: return subtract(lhs, rhs)复制代码
But what if a does not implement __sub__()? If a and b are different types, then we will try to call b's __rsub__() (the "r" in __rsub__ means "right", which means on the right side of the operator).
When both sides of the operation are of different types, this ensures that they both have a chance to try to make the expression valid. When they are the same, we assume that __sub__() will handle it. However, even if both implementations are identical, you still have to call __rsub__() in case one of the objects is a (sub)class of the other.
Now, both sides of the expression can participate in the operation! But what if for some reason an object's type doesn't support subtraction (e.g. 4 - "stuff" is not supported)? In this case, all __sub__ or __rsub__ can do is return NotImplemented.
This is the signal returned to Python that it should continue to the next operation and try to make the code run normally. For our code, this means that the method's return value needs to be checked before we can assume it works.
# 减法的实现,其中表达式的左侧和右侧均可参与运算_MISSING = object()def sub(lhs: Any, rhs: Any, /) -> Any: # lhs.__sub__ lhs_type = type(lhs) try: lhs_method = debuiltins._mro_getattr(lhs_type, "__sub__") except AttributeError: lhs_method = _MISSING # lhs.__rsub__ (for knowing if rhs.__rub__ should be called first) try: lhs_rmethod = debuiltins._mro_getattr(lhs_type, "__rsub__") except AttributeError: lhs_rmethod = _MISSING # rhs.__rsub__ rhs_type = type(rhs) try: rhs_method = debuiltins._mro_getattr(rhs_type, "__rsub__") except AttributeError: rhs_method = _MISSING call_lhs = lhs, lhs_method, rhs call_rhs = rhs, rhs_method, lhs if lhs_type is not rhs_type: calls = call_lhs, call_rhs else: calls = (call_lhs,) for first_obj, meth, second_obj in calls: if meth is _MISSING: continue value = meth(first_obj, second_obj) if value is not NotImplemented: return value else: raise TypeError( f"unsupported operand type(s) for -: {lhs_type!r} and {rhs_type!r}" )复制代码
If you look at the documentation for __rsub__(), you will notice a comment. It says that if the right side of a subtraction expression is a subclass of the left side (a true subclass, not the same class), and the __rsub__() methods of the two objects are different, then __sub__() will be called before Call __rsub__() first. In other words, if b is a subclass of a, the order of calls is reversed.
This may seem like a strange exception, but there is a reason behind it. When you create a subclass, it means you inject new logic on top of the operations provided by the parent class. This kind of logic does not have to be added to the parent class, otherwise the parent class will easily override the operations that the subclass wants to implement when operating on the subclass.
Specifically, suppose there is a class named Spam. When you execute Spam() - Spam(), you get an instance of LessSpam. Then you create a subclass of Spam named Bacon, so that when you subtract Bacon from Spam, you get VeggieSpam.
Without the above rules, Spam() - Bacon() would give LessSpam because Spam doesn't know that subtracting Bacon should give VeggieSpam.
但是,有了上述规则,就会得到预期的结果 VeggieSpam,因为 Bacon.__rsub__() 首先会在表达式中被调用(如果计算的是 Bacon() - Spam(),那么也会得到正确的结果,因为首先会调用 Bacon.__sub__(),因此,规则里才会说两个类的不同的方法需有区别,而不仅仅是一个由 issubclass() 判断出的子类。)
# Python中减法的完整实现_MISSING = object()def sub(lhs: Any, rhs: Any, /) -> Any: # lhs.__sub__ lhs_type = type(lhs) try: lhs_method = debuiltins._mro_getattr(lhs_type, "__sub__") except AttributeError: lhs_method = _MISSING # lhs.__rsub__ (for knowing if rhs.__rub__ should be called first) try: lhs_rmethod = debuiltins._mro_getattr(lhs_type, "__rsub__") except AttributeError: lhs_rmethod = _MISSING # rhs.__rsub__ rhs_type = type(rhs) try: rhs_method = debuiltins._mro_getattr(rhs_type, "__rsub__") except AttributeError: rhs_method = _MISSING call_lhs = lhs, lhs_method, rhs call_rhs = rhs, rhs_method, lhs if ( rhs_type is not _MISSING # Do we care? and rhs_type is not lhs_type # Could RHS be a subclass? and issubclass(rhs_type, lhs_type) # RHS is a subclass! and lhs_rmethod is not rhs_method # Is __r*__ actually different? ): calls = call_rhs, call_lhs elif lhs_type is not rhs_type: calls = call_lhs, call_rhs else: calls = (call_lhs,) for first_obj, meth, second_obj in calls: if meth is _MISSING: continue value = meth(first_obj, second_obj) if value is not NotImplemented: return value else: raise TypeError( f"unsupported operand type(s) for -: {lhs_type!r} and {rhs_type!r}" )复制代码
解决掉了减法运算,那么其它二元运算又如何呢?好吧,事实证明它们的操作相同,只是碰巧使用了不同的特殊/魔术方法名称。
所以,如果我们可以推广这种方法,那么我们就可以实现 13 种操作的语义:+ 、-、*、@、/、//、%、**、<<、>>、&、^、和 |。
由于闭包和 Python 在对象自省上的灵活性,我们可以提炼出 operator 函数的创建。
# 一个创建闭包的函数,实现了二元运算的逻辑_MISSING = object()def _create_binary_op(name: str, operator: str) -> Any: """Create a binary operation function. The `name` parameter specifies the name of the special method used for the binary operation (e.g. `sub` for `__sub__`). The `operator` name is the token representing the binary operation (e.g. `-` for subtraction). """ lhs_method_name = f"__{name}__" def binary_op(lhs: Any, rhs: Any, /) -> Any: """A closure implementing a binary operation in Python.""" rhs_method_name = f"__r{name}__" # lhs.__*__ lhs_type = type(lhs) try: lhs_method = debuiltins._mro_getattr(lhs_type, lhs_method_name) except AttributeError: lhs_method = _MISSING # lhs.__r*__ (for knowing if rhs.__r*__ should be called first) try: lhs_rmethod = debuiltins._mro_getattr(lhs_type, rhs_method_name) except AttributeError: lhs_rmethod = _MISSING # rhs.__r*__ rhs_type = type(rhs) try: rhs_method = debuiltins._mro_getattr(rhs_type, rhs_method_name) except AttributeError: rhs_method = _MISSING call_lhs = lhs, lhs_method, rhs call_rhs = rhs, rhs_method, lhs if ( rhs_type is not _MISSING # Do we care? and rhs_type is not lhs_type # Could RHS be a subclass? and issubclass(rhs_type, lhs_type) # RHS is a subclass! and lhs_rmethod is not rhs_method # Is __r*__ actually different? ): calls = call_rhs, call_lhs elif lhs_type is not rhs_type: calls = call_lhs, call_rhs else: calls = (call_lhs,) for first_obj, meth, second_obj in calls: if meth is _MISSING: continue value = meth(first_obj, second_obj) if value is not NotImplemented: return value else: exc = TypeError( f"unsupported operand type(s) for {operator}: {lhs_type!r} and {rhs_type!r}" ) exc._binary_op = operator raise exc复制代码
有了这段代码,你可以将减法运算定义为 _create_binary_op(“sub”, “-”),然后根据需要重复定义出其它运算。
想了解更多编程学习,敬请关注php培训栏目!
The above is the detailed content of Detailed explanation of binary arithmetic operations in Python. For more information, please follow other related articles on the PHP Chinese website!