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下实现的RSA加密/解密及签名/验证功能示例
Jul 17 Python
Python操作csv文件实例详解
Jul 31 Python
numpy中索引和切片详解
Dec 15 Python
Python处理文本换行符实例代码
Feb 03 Python
python编程测试电脑开启最大线程数实例代码
Feb 09 Python
利用python如何处理百万条数据(适用java新手)
Jun 06 Python
python 产生token及token验证的方法
Dec 26 Python
深度辨析Python的eval()与exec()的方法
Mar 26 Python
Python3 执行系统命令并获取实时回显功能
Jul 09 Python
python七种方法判断字符串是否包含子串
Aug 18 Python
golang中的空接口使用详解
Mar 30 Python
解决Pytorch修改预训练模型时遇到key不匹配的情况
Jun 05 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
处理(php-cgi.exe - FastCGI 进程超过了配置的请求超时时限)的问题
2013/07/03 PHP
PHP采用curl模仿用户登陆新浪微博发微博的方法
2014/11/07 PHP
PHP验证码类ValidateCode解析
2017/01/07 PHP
PHP实现简单的模板引擎功能示例
2017/09/02 PHP
phpStorm2020 注册码
2020/09/17 PHP
元素的内联事件处理函数的特殊作用域在各浏览器中存在差异
2011/01/12 Javascript
JavaScript包装对象使用介绍
2013/08/29 Javascript
js将json格式内容转换成对象的方法
2013/11/01 Javascript
JavaScript使用focus()设置焦点失败的解决方法
2014/09/03 Javascript
jQuery简单实现日历的方法
2015/05/04 Javascript
Angularjs的ng-repeat中去除重复数据的方法
2016/08/05 Javascript
Bootstrap Modal遮罩弹出层(完整版)
2016/11/21 Javascript
过期软件破解办法实例详解
2017/01/04 Javascript
JSON键值对序列化和反序列化解析
2017/01/24 Javascript
[48:38]DOTA2亚洲邀请赛 3.31 小组赛 B组 Mineski vs Secret
2018/03/31 DOTA
python网络编程实例简析
2014/09/26 Python
Phantomjs抓取渲染JS后的网页(Python代码)
2016/05/13 Python
python之PyMongo使用总结
2017/05/26 Python
Python日期的加减等操作的示例
2017/08/15 Python
python Spyder界面无法打开的解决方法
2018/04/27 Python
python指定写入文件时的编码格式方法
2018/06/07 Python
Python测试网络连通性示例【基于ping】
2018/08/03 Python
查看python安装路径及pip安装的包列表及路径
2019/04/03 Python
Python3 webservice接口测试代码详解
2020/06/23 Python
Python二元算术运算常用方法解析
2020/09/15 Python
CSS3绘制有活力的链接下划线
2016/07/14 HTML / CSS
WSDL的操作类型主要有几种
2013/07/19 面试题
大学生毕业求职找工作的自我评价
2013/09/29 职场文书
工商管理专业应届生求职信
2013/11/04 职场文书
网络维护管理员的自我评价分享
2013/11/11 职场文书
2015大学生求职信范文
2015/03/20 职场文书
辩论赛开场白大全(主持人+辩手)
2015/05/29 职场文书
学习雷锋主题班会
2015/08/14 职场文书
2019入党申请书格式和范文
2019/06/25 职场文书
CSS3新特性详解(五):多列columns column-count和flex布局
2021/04/30 HTML / CSS
Java Spring 控制反转(IOC)容器详解
2021/10/05 Java/Android