Introduction
The square root of a number is a very common mathematical function used in all aspects of science - physics, mathematics, computer science, etc. Square roots of numbers and expressions are very frequent in formulas in all matters of science, and especially in the way we represent reality - by modeling what we can observe with calculus.
In this article, we'll take a look at various ways to calculate a square root of a number in Python. Finally, we'll do a Performance Benchmark with constant and random numbers, as well as lists of random numbers to put all of the approaches to the test.
Calculate Square Root in Python with NumPy
NumPy is a scientific computation library, which found itself present in many applications and use cases. Naturally, it has many wrappers of mathematical functions as helper methods.
If not already installed, you can install it via pip
:
$ pip install numpy
In terms of NumPy, the sqrt()
function calculates the square root of a number, and returns the result:
import numpy as np
x = np.sqrt(2)
print(x)
This results in:
1.4142135623730951
Aside from taking a single variable as an argument, sqrt()
is also able to parse through lists and return a list of square roots:
arr = [2, 3, 5, 7]
roots = np.sqrt(arr)
print(roots)
This results in:
[1.41421356 1.73205081 2.23606798 2.64575131]
The sqrt()
function has a limitation though - it cannot calculate a square root of a negative number, because the square root operation with real numbers is only defined for positive numbers.
Attempting to insert -4
into the sqrt()
function will result in an exception:
print(np.sqrt(-4))
Trying to calculate a square root of a negative number will result with a warning and a nan
value:
RuntimeWarning: invalid value encountered in sqrt
nan
Calculate Square Root of Complex Number with Numpy
Fortunately, NumPy isn't constrained to only work with real numbers - it can work with complex numbers as well:
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}")
If there's at least one complex number in a list, all of the numbers will be cast to and treated as complex, so even negative integers can be added:
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]
Python's math Module
The math
module is a standard module packaged with Python. It's always available, but has to be imported, and provides wrappers for some common functions, such as the square root, powers, etc:
import math
math.sqrt()
The sqrt()
function of the math
module is a straightforward function that returns the square root of any positive number:
print(math.sqrt(2))
This results in:
1.4142135623730951
Unlike NumPy's sqrt()
function, it can only work on a single element, so if you want to calculate the square root of all elements in a list, you'll have to use a for
loop or a list comprehension:
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]
Check out our hands-on, practical guide to learning Git, with best-practices, industry-accepted standards, and included cheat sheet. Stop Googling Git commands and actually learn it!
In both cases, the roots
list will contain:
[1.4142135623730951, 1.7320508075688772, 2.23606797749979, 2.6457513110645907]
math.pow()
A square root of a number can also be calculated by raising a number to a power of ½:
$$
\sqrt x = x^{\frac 1 2}
$$
So really, finding the square root of a number can be expressed as raising the number to a power of ½. math.pow()
takes two arguments - the base and the exponent, and rises the base to the power of an exponent:
print(math.pow(2, 0.5))
Naturally, this results in:
1.4142135623730951
The ** Operator
The **
operator is a binary operator which means it works with two values, just like regular multiplication with *
does. However, as it is an operator used for exponentiation, we raise its left argument to the power of its right argument.
This approach can be used in the same form as the previous one:
print(2 ** 0.5)
And it also results in:
1.4142135623730951
The pow() Function
Python has another, built-in pow()
method that doesn't require an import of the math
module. This method is technically different from the math.pow()
method internally.
math.pow()
implicitly casts elements to doubles, while pow()
uses the object's internal implementation, based around the **
operator. While this difference in implementation may warrant the use of one or the other in certain contexts, if you're just calculating the square root of a number, you won't really see the difference:
print(pow(2, 0.5))
This results in:
1.4142135623730951
Performance Benchmark
So which one yields the best performance, and which one should you choose? As usual, there isn't one clear cut winner, and it depends on the usage of the methods. Namely, if you're working with constant numbers, random numbers or an array of random numbers on a larger scale - these methods will perform differently.
Let's test them all out on constant numbers, random numbers and arrays of random numbers:
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))
We've passed all of the methods outlined above through the same test - a constant number (which is likely to be cached for optimization), a random number on each of the 100k iterations, and a list of 100 random numbers.
Note: Only the relative numbers on each test compared to other methods in that test are relevant, since it takes more time to generate 100 random numbers than using the (cached) constant value.
Running this piece of code results in:
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
With constant numbers - the math.pow()
, math.sqrt()
and pow()
functions significantly outperform NumPy's sqrt()
function, as they can better utilize caching in the CPU on the language-level.
With random numbers, the caching doesn't work as well and we see smaller discrepancies.
With lists of random numbers, np.sqrt()
outperforms all three built-in methods significantly, and the **
operator performs in the same ball-park.
To summarize:
- For constant numbers, the
**
operator clearly performs the best on the test machine, executing 16 times faster than the built-in methods. - For random numbers,
np.sqrt()
outperforms the built-in methods, and the**
operator, though, there isn't any significant discrepancy in the results. - For random arrays, the
np.sqrt()
function outperforms the built-in methods, but the**
operator is very close.
Depending on the concrete input you're dealing with - you'll choose between these functions. While it may seem like they'll all perform well, and while in most cases, it won't make much of a difference, when dealing with huge datasets, even a 10% decrease in processing time can help in the long-run.
Depending on the data you're processing - test the different approaches on your local machine.
Conclusion
In this short article, we've taken a look at several ways to compute the Square Root of a number in Python.
We've taken a look at the math
module's pow()
and sqrt()
functions, as well as the built-in pow()
function, NumPy's sqrt()
function and the **
operator. Finally, we've benchmarked the methods to compare their performance on different types of input - constant numbers, random numbers and lists of random numbers.