Вычисление квадратного корня из числа в Python

Вступление

Квадратный корень из числа – очень распространенная математическая функция, используемая во всех областях науки – физике, математике, информатике и т.д. Квадратные корни чисел и выражений очень часто встречаются в формулах во всех областях науки, и особенно в том, как мы представляем реальность – моделируя то, что мы можем наблюдать с помощью исчисления.

В этой статье мы рассмотрим различные способы вычисления квадратного корня из числа в Python. Наконец, мы проведем тест производительности с постоянными и случайными числами, а также со списками случайных чисел, чтобы проверить все подходы.

Вычисление квадратного корня в Python с помощью NumPy

NumPy – это библиотека научных вычислений, которая присутствовала во многих приложениях и вариантах использования. Естественно, в нем есть множество оболочек математических функций в качестве вспомогательных методов.

Если она еще не установлена, вы можете установить ее через pip:

$ pip install numpy

В терминах NumPy функция sqrt() вычисляет квадратный корень из числа и возвращает результат:

import numpy as np:
x = np.scrt(2)
print(x)

Это приводит к:

1.4142135623730951

Помимо использования одной переменной в качестве аргумента, sqrt() также может анализировать списки и возвращать список квадратных корней:

arr = [2, 3, 5, 7]
roots = np.sqrt(arr)
print(roots)

Это приводит к:

[1.41421356 1.73205081 2.23606798 2.64575131]

Функция sqrt(), однако, имеет ограничение – она не может вычислять квадратный корень из отрицательного числа, поскольку операция квадратного корня с действительными числами определена только для положительных чисел.

Попытка вставить -4 в функцию sqrt() приведет к исключению:

print(np.sqrt(-4))

Попытка вычислить квадратный корень из отрицательного числа приведет к появлению предупреждения и значению nan:

RuntimeWarning: invalid value encountered in sqrt nan

Вычисление квадратного корня из комплексного числа с помощью Numpy

К счастью, NumPy не ограничивается работой только с действительными числами – он также может работать с комплексными числами:

import numpy as np

complex_number = -1 + 1j
complex_array = [-2, 3, complex_number]

complex_root = np.sqrt(complex_number)
complex_array_roots = np.sqrt(complex_array)

print(f"Square root of '{complex_number}':\n {complex_root}")
print(f"Square roots of '{complex_array}':\n {complex_array_roots}")

Если в списке есть хотя бы одно комплексное число, все числа будут приведены и обработаны как сложные, поэтому можно добавить даже отрицательные целые числа:

Square root of '(-1+1j)':
 (0.45508986056222733+1.09868411346781j)
Square roots of '[-2, 3, (-1+1j)]':
 [0.        +1.41421356j 1.73205081+0.j         0.45508986+1.09868411j]

Модуль math в Python

Модуль math – это стандартный модуль, упакованный с Python. Он всегда доступен, но должен быть импортирован и предоставляет оболочки для некоторых общих функций, таких как квадратный корень, полномочия и т.д.:

import math
math.sqrt()

Функция sqrt() модуля math- это простая функция, которая возвращает квадратный корень из любого положительного числа:

print(math.sqrt(2))

Это приводит к:

1.4142135623730951

В отличие от функции sqrt() NumPy, она может работать только с одним элементом, поэтому, если вы хотите вычислить квадратный корень из всех элементов в списке, вам придется использовать цикл for или генератор списка:

import math

arr = [2, 3, 5, 7]
roots = []

for x in arr:
    roots.append(math.sqrt(x))

# OR
roots = [math.sqrt(x) for x in arr]

В обоих случаях список корней будет содержать:

[1.4142135623730951, 1.7320508075688772, 2.23606797749979, 2.6457513110645907]

math.pow()

Квадратный корень из числа также может быть вычислен путем возведения числа в степень ½:

√x = x1/2

Так что на самом деле, нахождение квадратного корня из числа может быть выражено как увеличение числа до степени ½. math.pow() принимает два аргумента – основание и показатель степени, и увеличивает основание до степени экспоненты:

print(math.pow(2, 0.5))

Естественно, это приводит к:

1.4142135623730951

Оператор **

Оператор ** является двоичным оператором, что означает, что он работает с двумя значениями, как и обычное умножение с помощью *. Однако, поскольку это оператор, используемый для возведения в степень, мы повышаем его левый аргумент до степени его правого аргумента.

Этот подход может быть использован в той же форме, что и предыдущий:

print(2 ** 0.5)

И это также приводит к:

1.4142135623730951

Функция pow()

В Python есть еще один встроенный метод pow(), который не требует импорта математического модуля. Этот метод отличается от метода math.pow() внутренне.

math.pow() неявно преобразует элементы в двойные, в то время как pow() использует внутреннюю реализацию объекта, основанную на операторе **. Хотя это различие в реализации может оправдать использование того или иного в определенных контекстах, если вы просто вычисляете квадратный корень из числа, вы на самом деле не увидите разницы:

print(pow(2, 0.5))

Это приводит к:

1.4142135623730951

Контрольный показатель производительности

Итак, какой из них дает наилучшую производительность, и какой из них вы должны выбрать? Как обычно, нет одного явного победителя, и это зависит от использования методов. А именно, если вы работаете с постоянными числами, случайными числами или массивом случайных чисел в большем масштабе – эти методы будут работать по-другому.

Давайте проверим их все на постоянных числах, случайных числах и массивах случайных чисел:

import timeit

print("Time to execute 100k operations on constant number: \n")
print("math.sqrt(): %ss" % timeit.timeit("math.sqrt(100)", setup="import math", number=100000))
print("math.pow(): %ss" % timeit.timeit("math.pow(100, 0.5)", setup="import math", number=100000))
print("pow(): %ss" % timeit.timeit("pow(100, 0.5)", number=100000))
print("np.sqrt(): %ss" % timeit.timeit("np.sqrt(100)", setup="import numpy as np", number=100000))
print("** operator: %ss" % timeit.timeit("100 ** 0.5", number=100000))

print("\nTime to execute 100k operations on random number: \n")
print("math.sqrt() %ss" % timeit.timeit("math.sqrt(random.random())", setup="import math; import random;", number=100000))
print("math.pow(): %ss" % timeit.timeit("math.pow(random.random(), 0.5)", setup="import math; import random", number=100000))
print("pow(): %ss" % timeit.timeit("pow(random.random(), 0.5)", setup="import random", number=100000))
print("np.sqrt(): %ss" % timeit.timeit("np.sqrt(random.random())", setup="import numpy as np; import random", number=100000))
print("** operator: %ss" % timeit.timeit("random.random() ** 0.5", setup="import random", number=100000))

print("\nTime to execute 100k operations on list of random numbers: \n")
print("math.sqrt() %ss" % timeit.timeit("[math.sqrt(x) for x in np.random.rand(100)]", setup="import math; import numpy as np;", number=100000))
print("math.pow(): %ss" % timeit.timeit("[math.pow(x, 0.5) for x in np.random.rand(100)]", setup="import math; import numpy as np;", number=100000))
print("pow(): %ss" % timeit.timeit("[pow(x, 0.5) for x in np.random.rand(100)]", setup="import numpy as np;", number=100000))
print("np.sqrt(): %ss" % timeit.timeit("np.sqrt(np.random.rand(100))", setup="import numpy as np; import numpy as np;", number=100000))
print("** operator: %ss" % timeit.timeit("np.random.rand(100) ** 0.5", setup="import numpy as np", number=100000))

Мы прошли все описанные выше методы через один и тот же тест – постоянное число (которое, вероятно, будет кэшировано для оптимизации), случайное число на каждой из 100 тыс. итераций и список из 100 случайных чисел.

Примечание: Важны только относительные числа в каждом тесте по сравнению с другими методами в этом тесте, поскольку для генерации 100 случайных чисел требуется больше времени, чем при использовании (кэшированного) постоянного значения.

Выполнение этого фрагмента кода приводит к:

Time to execute 100k operations on constant number: 

math.sqrt(): 0.014326499999999999s
math.pow(): 0.0165132s
pow(): 0.018766599999999994s
np.sqrt(): 0.10575379999999998s
** operator: 0.0006493000000000193s

Time to execute 100k operations on random number: 

math.sqrt() 0.019939999999999958s
math.pow(): 0.022284300000000035s
pow(): 0.0231711s
np.sqrt(): 0.09066460000000004s
** operator: 0.018928s

Time to execute 100k operations on list of random numbers: 

math.sqrt() 2.7786073s
math.pow(): 2.9986906s
pow(): 3.5157339999999992s 
np.sqrt(): 0.2291957s
** operator: 0.2376024000000001s

С постоянными числами – функции math.pow(), math.sqrt() и pow() значительно превосходят функцию Numpy sqrt(), поскольку они могут лучше использовать кэширование в процессоре на уровне языка.

Со случайными числами кэширование работает не так хорошо, и мы видим меньшие расхождения.

Со списками случайных чисел np.sqrt() значительно превосходит все три встроенных метода, и оператор ** работает в одной и той же области действия.

Подводя итоги:

  • Для постоянных чисел оператор ** явно работает лучше всего на тестовой машине, выполняя в 16 раз быстрее, чем встроенные методы.
  • Для случайных чисел np.sqrt() превосходит встроенные методы и оператор **, хотя в результатах нет существенных расхождений.
  • Для случайных массивов функция np.sqrt() превосходит встроенные методы, но оператор ** очень близок.

В зависимости от конкретного ввода, с которым вы имеете дело, вы будете выбирать между этими функциями. Хотя может показаться, что все они будут работать хорошо, и хотя в большинстве случаев это не будет иметь большого значения, при работе с огромными наборами данных даже сокращение времени обработки на 10 % может помочь в долгосрочной перспективе.

В зависимости от обрабатываемых данных – протестируйте различные подходы на своем локальном компьютере.

Вывод

В этой короткой статье мы рассмотрели несколько способов вычисления квадратного корня из числа в Python.

Мы рассмотрели функции pow() и sqrt() математического модуля, а также встроенную функцию pow(), функцию Numpy sqrt() и оператор **. Наконец, мы провели сравнительный анализ методов для сравнения их производительности на различных типах входных данных – постоянных числах, случайных числах и списках случайных чисел.

Ответить