0

I am relatively familiar with Python, but might have forgotten how it is expected to behave in certain instances, as I've been working more lately with Rust, which has a lot of memory protections. I should stipulate that I'm using Python 3.9.4.

I'm working some Project Euler problems for practice and am writing unit tests using the unittest framework to go along with my code. What is surprising me is that the values of local variables--namely, a list--seem to be persisting across function calls. I am wondering if this is expected behaviour, either of Python in general or of the unittest framework? I must admit that I was quite surprised. My example is below.

This code is for Problem 3 of Project Euler. (I believe it's ok to share solutions to the first 100 problems, so I'm not violating any Project Euler's agreements). My code finds the prime factors (including repeats) of a given non-negative number, it looks like this:

import math

# Returns prime factors of a given non-negative number
def prime_factors(n, primes=[]):
    if n < 0:
        raise ValueError('Negative values are not valid')
    if n < 2:
        return primes
    for i in range(2, math.floor(n/2) + 1):
        if n % i == 0:
            primes.append(i)
            return prime_factors(n/i, primes=primes)
    
    primes.append(n)
    return primes

I've written some unittests to go along with this code (I've abbreviated slightly for clarity):

import unittest

class LargestPrimeTestCase(unittest.TestCase):
        
    def test_prime_factors(self):
        
        # Test obvious simple cases
        self.assertEqual([2], prime_factors(2))
        self.assertEqual([3], prime_factors(3))

I would expect these tests to pass, but they fail on the second assert statement. Namely, I'm receiving an AssertionError:

Traceback (most recent call last):
  File "/path/to/file", line 20, in test_largest_prime
    self.assertEqual([3], prime_factors(3))
AssertionError: Lists differ: [3] != [2, 3]

Yet, if I run just the first or the second line without the other, with a line commented out, the test passes. I've tested my code with more complex cases, so I know that in individual cases, the function correctly identifies the prime factors of a given number. What it appears to me, is that the values stored in primes are being persisted across function calls, even if I call the function without stipulating the value of primes, which should reset the value of the variable. I.e. calling

x = prime_factors(2)
y = prime_factors(3)
print(x)
print(y)

should result in:

[2]
[3]

and not:

[2]
[2, 3]

Yet, the latter seems to be occurring.

If I extract the second line of the test to another function, say:

    def test_prime_factors_2(self):
        self.assertEqual([3], prime_factors(3))

I'm still receiving the same error.

I tried adding some setUp and tearDown functions:

    def setUp(self):
        primes = []

    def tearDown(self):
        primes=[]

But this also did not solve the problem.

Is this expected behavior of unittest? Is this my own confusion with Python about how variables are stored and copied? I was able to fix the problem by modifying my code to:

import math
import copy

# Returns prime factors of a given non-negative number
def prime_factors(n, primes=[]):
    prime_numbers = copy.deepcopy(primes) # Adding a deepcopy here to avoid Python copying issues?
    if n < 0:
        raise ValueError('Negative values are not valid')
    if n < 2:
        return prime_numbers
    for i in range(2, math.floor(n/2) + 1):
        if n % i == 0:
            prime_numbers.append(i)
            return prime_factors(n/i, primes=prime_numbers)
    
    prime_numbers.append(n)
    return prime_numbers

This suggests to me that my issue is related to the way Python copies values. Would someone be able to enlighten me here?

martineau
  • 119,623
  • 25
  • 170
  • 301
Michael Pinkard
  • 205
  • 2
  • 10
  • 1
    TL;DR, don't use a mutable default argument unless you know for sure that it will never be mutated. – Carcigenicate Jul 02 '21 at 19:15
  • Thanks! I was having a hard time finding the keywords I was looking for – Michael Pinkard Jul 02 '21 at 19:23
  • 1
    `def prime_factors(n, primes=[]):` the default value is evaluated once when the function is defined. Each time you use the default argument, it retrieves the *same object* – juanpa.arrivillaga Jul 02 '21 at 19:24
  • As an aside, your `setUp` and `tearDown` methods do nothing useful. `primes = []` creates a new list, and assigns it to a local variable `primes`. This list is discarded when the function terminates. It has absolutely no connection to the parameter `primes` in your `prime_factors` function. – juanpa.arrivillaga Jul 02 '21 at 19:26
  • 1
    For reference then, instead of using a deepcopy of primes, it's Pythonic to use: `def prime_factors(n, primes=None):` and then define `primes` if it does not exist: `if primes is None: primes = []` – Michael Pinkard Jul 02 '21 at 19:27
  • @juanpa.arrivillaga Yes, that became clear. It was more like desperation to try them! – Michael Pinkard Jul 02 '21 at 19:29

0 Answers0