Разница между встроенным pow() и math.pow() для float, в Python?

Есть ли разница в результатах, возвращаемых встроенным в Python pow(x, y) (без третьего аргумента) и значения, возвращаемые math.pow(), в случае двух аргументов с плавающей точкой.

Я задаю этот вопрос, потому что документация для math.pow() подразумевает, что pow(x, y) (т.е. x**y) по сути так же, как math.pow(x, y):

math.pow(x, y)

Верните x в степень y. Исключительные случаи, насколько это возможно, следуют Приложению "F" стандарта C99. В частности, pow(1.0, x) и pow(x, 0.0) всегда возвращают 1.0, даже когда x равен нулю или NaN. Если и x, и y конечны, x отрицателен, а y не является целым числом, тогда pow(x, y) не определен и вызывает ValueError.

Изменено в версии 2.6: результат 1**nan и nan**0 не определен.

Обратите внимание на последнюю строку: документация подразумевает, что поведение math.pow() это оператор возведения в степень ** (и, следовательно, pow(x, y)). Это официально гарантировано?

Предыстория: моя цель состоит в том, чтобы обеспечить реализацию как встроенного pow() и из math.pow() для чисел с неопределенностью, которые ведут себя так же, как и с обычными числами типа Python (те же числовые результаты, те же исключения, те же результаты для угловых случаев и т. д.). Я уже реализовал кое-что, что работает довольно хорошо, но есть некоторые угловые случаи, которые необходимо обработать.

3 ответа

Решение

Быстрая проверка

По сигнатурам мы можем сказать, что они разные:

Pow (x, y [, z])

math.pow (x, y)

Кроме того, попытка сделать это в оболочке даст вам быстрое представление:

>>> 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 класс, чтобы справиться с Uncertain экземпляр в качестве входа в __init__()

math.pow() неявно преобразует свои аргументы в float:

>>> 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 а также ** определяя __pow__ а также __rpow__ методы для вашего класса.

Тем не менее, вы не можете перегружать math.pow (без хаков вроде math.pow = pow). Вы можете сделать класс пригодным для использования с math.pow определив __float__ преобразование, но тогда вы потеряете неопределенность, связанную с вашими числами.

Стандарт 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,

Just adding %timeit comparison

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)
Другие вопросы по тегам