在Python中,float类型的内置pow()和math.pow()之间有什么区别?

72
Python内置函数pow(x, y)(没有第三个参数)和math.pow()在两个float参数的情况下返回的结果有区别吗?
我问这个问题是因为math.pow()documentation暗示pow(x, y)(即x**y)基本上与math.pow(x, y)相同:
请注意最后一行:文档暗示math.pow()的行为与指数运算符**(因此也与pow(x, y))相同。这是否得到官方保证?
背景:我的目标是提供一个针对带有不确定性数字的内置pow()math.pow()的实现,其行为与常规Python浮点数相同(相同的数值结果,相同的异常,角落情况下相同的结果等)。 我已经已经实现了一些工作得很好,但还需要处理一些角落情况

1
可能你已经知道了,但这里有一些人在尝试它:http://www.velocityreviews.com/forums/t355507-pow-power-function.html - yurisich
4个回答

61

快速检查

从函数签名中,我们可以看出它们是不同的:

pow(x, y[, z])

math.pow(x, y)

此外,在Shell中尝试一下会给您一个快速了解:

>>> pow is math.pow
False

测试差异

了解这两个函数之间行为差异的另一种方法是进行测试:

import math
import traceback
import sys

inf = float("inf")
NaN = float("nan")

vals = [inf, NaN, 0.0, 1.0, 2.2, -1.0, -0.0, -2.2, -inf, 1, 0, 2]

tests = set([])

for vala in vals:
  for valb in vals:
    tests.add( (vala, valb) )
    tests.add( (valb, vala) )


for a,b in tests:
  print("math.pow(%f,%f)"%(a,b) )
  try:
    print("    %f "%math.pow(a,b))
  except:
    traceback.print_exc()
  
  print("__builtins__.pow(%f,%f)"%(a,b) )
  try:
    print("    %f "%__builtins__.pow(a,b))
  except:
    traceback.print_exc()

我们可以注意到一些细微的差别。例如:

math.pow(0.000000,-2.200000)
    ValueError: math domain error

__builtins__.pow(0.000000,-2.200000)
    ZeroDivisionError: 0.0 cannot be raised to a negative power

还有其他的区别,上面的测试列表并不完整(没有长数字,没有复杂类型等等...)但是这将给我们一个实用的列表来表明这两个函数的行为有所不同。我还建议扩展上述测试以检查每个函数返回的类型。你可以编写类似的东西,创建两个函数之间差异的报告。

math.pow()

math.pow() 与内置的 **pow() 相比,处理其参数的方式非常不同。这是以灵活性为代价的。看一下 源代码,我们可以看到 math.pow() 的参数直接转换为双精度浮点数

static PyObject *
math_pow(PyObject *self, PyObject *args)
{
    PyObject *ox, *oy;
    double r, x, y;
    int odd_y;

    if (! PyArg_UnpackTuple(args, "pow", 2, 2, &ox, &oy))
        return NULL;
    x = PyFloat_AsDouble(ox);
    y = PyFloat_AsDouble(oy);
/*...*/

对于双精度浮点数,检查它们的有效性后,将结果传递给底层C数学库。

内置函数pow()

另一方面,内置的pow()(与**操作符相同)行为完全不同,它实际上使用了对象自己的**操作符实现,如果需要,用户可以通过替换数字的__pow__()__rpow__()__ipow__()方法来覆盖它。

对于内置类型,值得研究两种数字类型实现的幂函数之间的区别,例如浮点数长整型复数

覆盖默认行为

模拟数字类型的方法在这里描述。如果你正在为带有不确定性的数字创建新类型,你需要为该类型提供__pow__()__rpow__()和可能的__ipow__()方法。这将允许你的数字与运算符一起使用:

class Uncertain:
  def __init__(self, x, delta=0):
    self.delta = delta
    self.x = x
  def __pow__(self, other):
    return Uncertain(
      self.x**other.x, 
      Uncertain._propagate_power(self, other)
    )
  @staticmethod
  def _propagate_power(A, B):
    return math.sqrt(
      ((B.x*(A.x**(B.x-1)))**2)*A.delta*A.delta +
      (((A.x**B.x)*math.log(B.x))**2)*B.delta*B.delta
    )
为了重写 math.pow(),您需要进行猴子补丁以支持您的新类型:
def new_pow(a,b):
    _a = Uncertain(a)
    _b = Uncertain(b)
    return _a ** _b

math.pow = new_pow

注意,为了使这个方法工作,你需要调整Uncertain类以便能够处理__init__()的输入参数是Uncertain实例的情况。


这是我正在寻找的答案类型:关于实数的pow(x, y)math.pow(x, y)之间差异的一些确切事实。 但是,还有一个缺失的部分:您知道如何针对实数实现__pow()__吗? 最重要的问题是:它是否像math.pow()那样使用底层的C数学库,并且是否以相同方式处理异常情况(包括它引发的异常)? 我快速查看了http://hg.python.org/cpython/file/6a60359556f9/Objects/floatobject.c#l807上的pow(x, y)源代码,但尚未得出结论。 - Eric O. Lebigot
1
__pow__ 最终会使用底层的数学库。它在你提供的文件中(http://hg.python.org/cpython/file/6a60359556f9/Objects/floatobject.c#l911)的第 911 行:ix = pow(iv, iw);。考虑到你想要做什么,我建议设置一个测试套件,并运行内置的 pow()math.pow(),以了解它们的行为。我已经添加了一个脚本,可以运行一系列的测试来达到这个目的。 - brice
1
顺便说一句,我看了一下Uncertainty,它看起来非常有用!另外,我想知道,在进行不确定性计算时,您是否考虑了底层精度? - brice
感谢您的好评。如果您所说的“底层精度”是指浮点精度,那么答案是“有时候”:大多数不确定性计算都是通过分析方法完成的(当然,公式是要进行数值计算的)。但是,用户也可以要求对于无法接受带有不确定性数字的Fortran函数进行数值不确定性计算;在这种情况下,我会考虑浮点数的精度(在计算数值导数时)。 - Eric O. Lebigot

40

math.pow()将其参数隐式转换为float

>>> from decimal import Decimal
>>> from fractions import Fraction
>>> math.pow(Fraction(1, 3), 2)
0.1111111111111111
>>> math.pow(Decimal(10), -1)
0.1

但是内置的 pow 不行:

>>> pow(Fraction(1, 3), 2)
Fraction(1, 9)
>>> pow(Decimal(10), -1)
Decimal('0.1')

我的目标是为带有不确定性的数字提供内置的pow()和math.pow()实现。

你可以通过定义类的__pow____rpow__方法来重载pow**

但是,你无法直接重载math.pow(除非像math.pow = pow这样的hack)。你可以通过定义__float__转换使得一个类能够使用math.pow,但这样你将失去与数字相关联的不确定性。


3
好的回答。就CPython源代码而言,内置的pow函数调用PyNumber_Power,后者会在相关对象上调用__pow__。例如,可以查看浮点数求幂(float_pow)的源代码。相比之下,math.pow的源代码确实会优先将其参数转换为浮点数。 - Ben Hoyt
5
你可能想补充一下,“math.pow”之所以存在是因为“math”模块反映了标准C语言的数学库。 - orlp
@dan04:你是在暗示除了浮点数转换之外,这两个函数的行为完全相同(包括数字结果、异常情况、边角值等)吗?这确实是问题所在。 :) - Eric O. Lebigot
@dan04:PS:Brice的回答表明,math.pow()和内置的pow()在使用两个浮点数参数时的行为是不同的。 - Eric O. Lebigot

12

Python标准库中的pow函数包含一个简单的技巧,使得pow(2, 3, 2)(2 ** 3) % 2更快(当然,只有在处理大数时才能体现出来)。

另一个重要的区别是这两个函数如何处理不同的输入格式。

>>> pow(2, 1+0.5j)
(1.8810842093664877+0.679354250205337j)
>>> math.pow(2, 1+0.5j)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: can't convert complex to float

然而,我不知道为什么有人会更喜欢使用math.pow而不是pow


1
为什么你把特殊情况的优化称作“简单的hack”? - Wolf
@Wolf 因为这就是特殊情况下的优化,我个人认为。它们确实有价值,但它们很简单,只能解决单一用例,因此它们是hack。 - Tom van der Woerdt
@TomvanderWoerdt,你是在解决不合理的函数重载设计缺陷吗?如果是的话,我同意:像powmod这样的名称更加合适。 - Wolf

1

只是添加%timeit比较

In [1]: def pair_generator(): 
    ...:     yield (random.random()*10, random.random()*10) 
    ...:   

In [2]: %timeit [a**b for a, b in pair_generator()]                                                                    
538 ns ± 1.94 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

In [3]: %timeit [math.pow(a, b) for a, b in pair_generator()]                                                          
632 ns ± 2.77 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

1
从这些结果中,结论实际上并不明显:事实上,在Python 3.6.2中,如果您检查5.2 ** 2.2的Python字节码,您将看到解释器计算它并用常量替换它(import dis; dis.dis(5.2**2.2))。因此,5.2 ** 2.2的计时结果不包括计算时间! - Eric O. Lebigot
1
@EricOLebigot 感谢您指出这一点。我已经编辑了帖子。 - zhukovgreen

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接