Python二元算术运算常用方法解析


Posted in Python onSeptember 15, 2020

在本文中,我想谈谈二元算术运算。具体来说,我想解读减法的工作原理:a - b。我故意选择了减法,因为它是不可交换的。这可以强调出操作顺序的重要性,与加法操作相比,你可能会在实现时误将 a 和 b 翻转,但还是得到相同的结果。

查看 C 代码

按照惯例,我们从查看 CPython 解释器编译的字节码开始。

>>> 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

看起来我们需要深入研究 BINARY_SUBTRACT 操作码。翻查 Python/ceval.c 文件,可以看到实现该操作码的 C 代码如下:

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(); 
}

来源: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__()实现减法 
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)

2、让右侧使用__rsub__()

但是,如果 a 没有实现__sub__() 怎么办?如果 a 和 b 是不同的类型,那么我们会尝试调用 b 的 __rsub__()(__rsub__ 里面的“r”表示“右”,代表在操作符的右侧)。

当操作的双方是不同类型时,这样可以确保它们都有机会尝试使表达式生效。当它们相同时,我们假设__sub__() 就能够处理好。但是,即使两边的实现相同,你仍然要调用__rsub__(),以防其中一个对象是其它的(子)类。

3、不关心类型

现在,表达式双方都可以参与运算!但是,如果由于某种原因,某个对象的类型不支持减法怎么办(例如不支持 4 - “stuff”)?在这种情况下,__sub__ 或__rsub__ 能做的就是返回 NotImplemented。

这是给 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 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}" 
      )

4、子类优先于父类

如果你看一下__rsub__() 的文档,就会注意到一条注释。它说如果一个减法表达式的右侧是左侧的子类(真正的子类,同一类的不算),并且两个对象的__rsub__() 方法不同,则在调用__sub__() 之前会先调用__rsub__()。换句话说,如果 b 是 a 的子类,调用的顺序就会被颠倒。

这似乎是一个很奇怪的特例,但它背后是有原因的。当你创建一个子类时,这意味着你要在父类提供的操作上注入新的逻辑。这种逻辑不一定要加给父类,否则父类在对子类操作时,就很容易覆盖子类想要实现的操作。

具体来说,假设有一个名为 Spam 的类,当你执行 Spam() - Spam() 时,得到一个 LessSpam 的实例。接着你又创建了一个 Spam 的子类名为 Bacon,这样,当你用 Spam 去减 Bacon 时,你得到的是 VeggieSpam。

如果没有上述规则,Spam() - Bacon() 将得到 LessSpam,因为 Spam 不知道减掉 Bacon 应该得出 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”, “-”),然后根据需要重复定义出其它运算。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Python 相关文章推荐
Python中的zip函数使用示例
Jan 29 Python
在Python中利用Pandas库处理大数据的简单介绍
Apr 07 Python
python任务调度实例分析
May 19 Python
Python实现的基于优先等级分配糖果问题算法示例
Apr 25 Python
解决python线程卡死的问题
Feb 18 Python
django组合搜索实现过程详解(附代码)
Aug 06 Python
基于matplotlib xticks用法详解
Apr 16 Python
使用sklearn对多分类的每个类别进行指标评价操作
Jun 11 Python
使用Keras实现简单线性回归模型操作
Jun 12 Python
Python 实现 T00ls 自动签到脚本代码(邮件+钉钉通知)
Jul 06 Python
Keras构建神经网络踩坑(解决model.predict预测值全为0.0的问题)
Jul 07 Python
Python离线安装openpyxl模块的步骤
Mar 30 Python
Python实现像awk一样分割字符串
Sep 15 #Python
详解Pycharm安装及Django安装配置指南
Sep 15 #Python
Java Unsafe类实现原理及测试代码
Sep 15 #Python
python装饰器实现对异常代码出现进行自动监控的实现方法
Sep 15 #Python
Python requests上传文件实现步骤
Sep 15 #Python
python -v 报错问题的解决方法
Sep 15 #Python
基于Python正确读取资源文件
Sep 14 #Python
You might like
浅析memcache启动以及telnet命令详解
2013/06/28 PHP
PHP验证信用卡卡号是否正确函数
2015/05/27 PHP
在js中使用&quot;with&quot;语句中跨frame的变量引用问题
2007/03/08 Javascript
JavaScript 判断判断某个对象是Object还是一个Array
2010/01/28 Javascript
提升你网站水平的jQuery插件集合推荐
2011/04/19 Javascript
js函数返回多个返回值的示例代码
2013/11/05 Javascript
jQuery实现流动虚线框的方法
2015/01/29 Javascript
JavaScript控制图片加载完成后调用回调函数的方法
2015/03/20 Javascript
遮罩层点击按钮弹出并且具有拖动和关闭效果(两种方法)
2015/08/20 Javascript
JavaScript实现的伸展收缩型菜单代码
2015/10/14 Javascript
javascript实现禁止复制网页内容汇总
2015/12/30 Javascript
多功能jQuery树插件zTree实现权限列表简单实例
2016/07/12 Javascript
Javascript 实现简单计算器实例代码
2016/10/23 Javascript
基于JS快速实现导航下拉菜单动画效果附源码下载
2016/10/27 Javascript
JS实现的适合做faq或menu滑动效果示例
2016/11/17 Javascript
BootStrap表单控件之复选框checkbox和单选择按钮radio
2017/05/23 Javascript
实现两个文本框同时输入的实例
2017/09/25 Javascript
React+Webpack快速上手指南(小结)
2018/08/15 Javascript
vue项目引入ts步骤(小结)
2019/10/31 Javascript
vue Element左侧无限级菜单实现
2020/06/10 Javascript
JS数组reduce()方法原理及使用技巧解析
2020/07/14 Javascript
[05:14]辉夜杯主赛事第二日 RECAP精彩回顾
2015/12/27 DOTA
[19:26]TNC vs EG (BO3)
2018/06/07 DOTA
python访问抓取网页常用命令总结
2017/04/11 Python
pytorch 把MNIST数据集转换成图片和txt的方法
2018/05/20 Python
Python在for循环中更改list值的方法【推荐】
2018/08/17 Python
django 实现后台从富文本提取纯文本
2020/07/02 Python
6种非常炫酷的CSS3按钮边框动画特效
2016/03/16 HTML / CSS
纯CSS3实现的井字棋游戏
2020/11/25 HTML / CSS
专门经营化妆刷的美国彩妆品牌:Sigma Beauty
2017/09/11 全球购物
理肤泉俄罗斯官网:La Roche-Posay俄罗斯
2018/07/24 全球购物
法国购买隐形眼镜和眼镜网站:Optical Center
2019/10/08 全球购物
《桂林山水》教学反思
2014/02/08 职场文书
庆国庆国旗下讲话稿2014
2014/09/21 职场文书
2019年教师节祝福语精选,给老师送上真诚的祝福
2019/09/09 职场文书
MySQL sql模式设置引起的问题
2022/05/15 MySQL