7
\$\begingroup\$

What is this:

This is a simple function, part of a basic Monte Carlo simulation. It takes sample size and probability as parameters. It returns the simulation result (positive answers) plus the input parameters in a tuple.

What I'm asking:

I'm trying to avoid using temporary variables, I have two questions.

  1. Do I really save memory by avoiding storing interim results?
  2. How could I improve readability without adding variables?

def simulate_survey(sample_size, percent_subscribes):
    return (
        sample_size,
        percent_subscribes,
        round(
            (
                sum([
                    r.random() < percent_subscribes 
                    for _ in range(sample_size)
                ]) / sample_size
            ),
            2
        )
    )

\$\endgroup\$
6
\$\begingroup\$
  • As I discovered recently, summing a lot of booleans, where the chance that the value is False is not negligible, can be surprisingly slow.

    So I would change your survey result calculation to:

    sum([1 for _ in range(sample_size) if r.random() < percent_subscribes])
    

    This allows sum to use its faster integer implementation and you do not sum a bunch of zeros.

  • Alternatively, you could look at this problem as an application of the binomial distribution. You have some chance that a certain result is obtained and you want to know how often that chance was true for some population. For this you can use numpy.random.binomial:

    import numpy as np
    
    def simulate_survey(sample_size, percent_subscribes):
        subscribers = np.random.binomial(sample_size, percent_subscribes)
        return sample_size, percent_subscribes, round(subscribers / sample_size, 2)
    

    Using numpy here may also speed up your process in other places. If you need to run this function many times, you probably want to use the third argument to generate multiple values at once.

    IMO, the readability is also greatly increased by using one temporary variable here, instead of your many levels of parenthesis.

  • I am not a fan of your function returning its inputs. The values of those should already be available in the scope calling this function, so this seems unnecessary. One exception would be that you have other, similar, functions which actually return different/modified values there.

  • You should add a docstring describing what your function does.

\$\endgroup\$
4
\$\begingroup\$

I think avoiding temporary variables, when we have no strict memory limit, is a bad idea. There is no way to have a readable code without using variables. So let's create a version of your code with temp variables:

def simulate_survey(sample_size, percent_subscribes):
    sum_result = sum([x for x in [True] * sample_size if r.random() < percent_subscribes])
    third_value = round(sum_result / sample_size, 2)
    return (
        sample_size,
        percent_subscribes,
        third_value
    )

It's not the most readable version of your code, But it's clearly more readable (I changed the way you created the sum value. I'm programming with Python for years, but that syntax is so strange to me. I hope my code do what your code did).

So Is there a huge memory usage gap between those programs? We now that Python does not remove temporary variables as a part of its optimization process (you can read more about it here). So obviously, my program should use more memory than yours. But how much?

I used resource module for comparing them. You can use this too if you are working on a UNIX based os.

Here is the code that I tried in both programs for measuring memory usage:

print(simulate_survey(64, 0.5))
print(resource.getrusage(resource.RUSAGE_SELF).ru_maxrss)

Your variable-less program shows values around 11860 KB, But my program with temporary variables used almost 12008 KB. There is 200 KB difference, but don't forget that my code is not completely the same as your code and I changed how it creates third value.

So let's change the third value to the way you creates that:

def simulate_survey(sample_size, percent_subscribes):
    sum_result = sum([
                    r.random() < percent_subscribes
                    for _ in range(sample_size)
                ])
    third_value = round(sum_result / sample_size, 2)
    return (
        sample_size,
        percent_subscribes,
        third_value
    )

So what happens if we test memory usage of this code that has the exact same logic as the first version? The result is around 11896 KB. Only between 10 to 30 KB more than the first version (Because each time we create a process, does not exactly same things happen, memory usage values are different each time).

So, as a conclusion, if you are not working on a machine with very tiny memory (something like embedded programming that is not common using python), I really recommend you that always use things like temporary variables to make your code readable.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.