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实现批量转换文件编码(批转换编码示例)
Jan 23 Python
Python中使用ConfigParser解析ini配置文件实例
Aug 30 Python
Python3实现并发检验代理池地址的方法
Sep 18 Python
Pandas 数据处理,数据清洗详解
Jul 10 Python
对Python3使运行暂停的方法详解
Feb 18 Python
详解django中url路由配置及渲染方式
Feb 25 Python
如何爬取通过ajax加载数据的网站
Aug 15 Python
Python解释器及PyCharm工具安装过程
Feb 26 Python
Python unittest框架操作实例解析
Apr 13 Python
Anaconda和ipython环境适配的实现
Apr 22 Python
Python基础之赋值,浅拷贝,深拷贝的区别
Apr 30 Python
Python中OpenCV实现查找轮廓的实例
Jun 08 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
DOTA2 无惧惊涛骇浪 昆卡大型水友攻略
2020/04/20 DOTA
在WordPress中使用wp_count_posts函数来统计文章数量
2016/01/05 PHP
php分页查询的简单实现代码
2017/03/14 PHP
PHP实现二叉树深度优先遍历(前序、中序、后序)和广度优先遍历(层次)实例详解
2018/04/20 PHP
图片按比例缩放函数
2006/06/26 Javascript
利用js跨页面保存变量做菜单的方法
2008/01/17 Javascript
使用JS+plupload直接批量上传图片到又拍云
2014/12/01 Javascript
JavaScript实现计算字符串中出现次数最多的字符和出现的次数
2015/03/12 Javascript
JS扩展方法实例分析
2015/04/15 Javascript
JS实现CheckBox复选框全选全不选功能
2015/05/06 Javascript
Node.js操作mysql数据库增删改查
2016/03/30 Javascript
AngularJs ng-change事件/指令的用法小结
2017/11/01 Javascript
详解angular2 控制视图的封装模式
2018/12/27 Javascript
封装微信小程序http拦截器过程解析
2019/08/13 Javascript
JavaScript数组及常见操作方法小结
2019/11/13 Javascript
JavaScript实现多文件下载方法解析
2020/08/07 Javascript
浅析VUE防抖与节流
2020/11/24 Vue.js
[02:43]2014DOTA2国际邀请赛 官方Alliance战队纪录片
2014/07/14 DOTA
python 切片和range()用法说明
2013/03/24 Python
Python AES加密模块用法分析
2017/05/22 Python
Python3实现带附件的定时发送邮件功能
2020/12/22 Python
python定间隔取点(np.linspace)的实现
2019/11/27 Python
Python 寻找局部最高点的实现
2019/12/05 Python
关于初始种子自动选取的区域生长实例(python+opencv)
2020/01/16 Python
python爬虫如何解决图片验证码
2021/02/14 Python
HTML5+Canvas+CSS3实现齐天大圣孙悟空腾云驾雾效果
2016/04/26 HTML / CSS
英国、欧洲和全球租车服务:Avis英国
2016/08/29 全球购物
瑞典领先的汽车零部件网上零售商:bildelaronline24.se
2017/01/12 全球购物
zooplus意大利:在线宠物商店
2019/08/07 全球购物
写好自荐信的几个要点
2013/12/26 职场文书
前台文员个人求职信范文
2014/01/05 职场文书
通用自荐信范文
2014/03/14 职场文书
商场开业庆典策划方案
2014/06/02 职场文书
2016年大学生就业指导课心得体会
2015/10/09 职场文书
springboot layui hutool Excel导入的实现
2022/03/31 Java/Android
Python中time标准库的使用教程
2022/04/13 Python