12
\$\begingroup\$

Problem:

In the following expression 1-2+3-4+5-6 ... -98+99-100, print result every 10th (10, 20, 30, ...) calculation.

My code:

result  = 0
operand = 1
counter = 0
for i in range(1, 101):
    result = result + (i * operand)
    counter = counter + 1
    operand = operand *  -1 #Convert between positive and negative number
    if counter >= 10:
        counter = 0
        print(result)

My problem:

I think this code has not good readability. For example, I used * -1 for changing positive and negative number. But it requires math skill(multiplying a negative number) to understand code.

And I don't sure variable name 'counter' and 'operand' is appropriate for this context.

Is there any suggestion?

\$\endgroup\$
  • \$\begingroup\$ Do you need the final result of the whole sum and just want to report progress as the summing is going on, or do you want the sum of only every 10th element and also print progress? \$\endgroup\$ – Graipher Dec 10 '18 at 15:29
  • 1
    \$\begingroup\$ Maybe I'm missing something, but isn't the alternating sum you want $$-\sum_{n=1}^{m} n (-1)^n \equiv -\frac{1}{4}-\frac{1}{4}(-1)^m (2m+1)$$ And then you can substitute m = 10n or whatever value you need. Far faster than looping. \$\endgroup\$ – esote Dec 11 '18 at 1:35
  • \$\begingroup\$ @Graipher I want to report progress as the summing is going on. but original question is not clear. So i think it should be interpreted like this. \$\endgroup\$ – jun Dec 11 '18 at 1:50
  • \$\begingroup\$ @esote this seems like the start of an answer \$\endgroup\$ – Mathias Ettinger Dec 11 '18 at 21:59
  • 1
    \$\begingroup\$ @esote Mathematical analysis of a problem in order to reduce code complexity are usually well received. \$\endgroup\$ – Mathias Ettinger Dec 11 '18 at 22:20
12
\$\begingroup\$

The first thing you can do to make the sign change obvious is to use operand = -operand, this way you avoid the need for the multiplication operator. Also changing the operand name to sign can help.

You also don't need counter as it will be equal to i before the test. Just change the test to use the modulo (%) operator instead of reseting counter.

And using augmented assignment for result can lead to a more readable line too:

result = 0
sign = 1
for i in range(1, 101):
    result += sign * i
    sign = -sign
    if i % 10 == 0:
        print(result)

Now, this probably need to be made more reusable, this means building a function out of this code and avoiding mixing computation and I/Os:

def alternating_sum(start=1, stop=100, step=10):
    result = 0
    sign = 1
    for i in range(start, stop+1):
        result += sign * i
        sign = -sign
        if i % step == 0:
            yield result

Usage being:

for result in alternating_sum(1, 100, 10):
    print(result)

Also, depending on the problem at hand, you can leverage the fact that every two numbers add-up to -1. So every 10 numbers add-up to -5. If you don't need much genericity, you can simplify the code down to:

def alternating_every_ten(end=100):
    reports = end // 10
    for i in range(1, reports + 1):
        print('After', 10 * i, 'sums, result is', -5 * i)
\$\endgroup\$
  • \$\begingroup\$ I would recommend writing expressions with a sign variable as sign * value to match the standard method of writing numbers. \$\endgroup\$ – Apollys Dec 10 '18 at 18:41
  • \$\begingroup\$ @Apollys Right, fixed. \$\endgroup\$ – Mathias Ettinger Dec 10 '18 at 20:11
6
\$\begingroup\$

You can get rid of your counter and directly use i by using modular arithmetic. i % 10 == 0 is true whenever i is divisible by 10.

You can get rid of your operand = operand * -1 by using itertools.cycle.

from itertools import cycle

result = 0
for i, sign in zip(range(1, 101), cycle([1, -1])):
    result += sign * i
    if i  % 10 == 0:
        print(result)

The generation of result itself would be a lot more concise with a generator comprehension, but this excludes the progress prints:

result = sum(sign * i for i, sign in zip(range(1, 101), cycle([1, -1])))
\$\endgroup\$
  • \$\begingroup\$ @MathiasEttinger: It seems like MaartenFabre is already going in that direction in their answer. \$\endgroup\$ – Graipher Dec 10 '18 at 15:25
  • \$\begingroup\$ Yep, saw that after the comment \$\endgroup\$ – Mathias Ettinger Dec 10 '18 at 15:28
5
\$\begingroup\$

instead of doing the +- by hand, you can use the fact that (-1) ** (i-1) * i for i in range(101) alternates the values

Further on, you can use itertools.accumulate and itertools.islice to take the cumulative sum and select every 10th number

numbers = (int((-1) ** (i-1) * i) for i in range(101))
cum_sum = itertools.accumulate(numbers)
numbers_10 = itertools.islice(cum_sum, 10, None, 10)

for i in numbers_10:
    print(i)
-5
-10
-15
-20
-25
-30
-35
-40
-45
-50

or alternatively, per 200_success' suggestion:

numbers = (sign * i for sign, i in zip(itertools.cycle((1, -1)), itertools.count(1)))
cum_sum = itertools.accumulate(numbers)
numbers_10 = itertools.islice(cum_sum, 9, 100, 10)
\$\endgroup\$
  • \$\begingroup\$ With this you do not get the final result, though. Maybe with a itertools.tee of the numbers, which you can then sum, but then it is no longer reporting on the progress... \$\endgroup\$ – Graipher Dec 10 '18 at 15:24
  • \$\begingroup\$ what do you mean with the final result? If needed, you need to do cum_sum = list(itertools.accumulate(numbers)) or numbers = [int((-1) ** (i-1) * i) for i in range(101)] \$\endgroup\$ – Maarten Fabré Dec 10 '18 at 15:37
  • 2
    \$\begingroup\$ I like this. Personally, I'd write it as cum_sum = accumulate(sign * n for sign, n in zip(cycle((+1, -1)), count())) to more elegantly express the idea of an infinite series, then extract just the relevant partial sums with for n in islice(cum_sum, 9, 100, 10): print(n). \$\endgroup\$ – 200_success Dec 11 '18 at 0:11
1
\$\begingroup\$

I would convert the sign flip into a generator, created via a generator comprehension, recognizing that evens should be negative:

#  Integers from 1-100, where evens are negative: 1, -2, 3, -4, 5...
sequence_gen = (i if i % 2 else -i for i in range(1,101))

Equivalent to:

def sequence_gen():
    for i in range(1, 101):
        if bool(i % 2):  # For our purposes this is i % 2 == 1:
            yield i
        else:
            yield -i

Then your code becomes:

result = 0
for index, number in enumerate(sequence_gen):
    result += number
    if index % 10 == 9:  # Note the comparison change by starting at 0
        print(result)

Note this is about half way to what Mathias proposed, and can be used in conjunction, the combination being:

def sequence_sums(start, stop, step):
    result = 0
    seq_gen = (i if i % 2 else -i for i in range(start, stop + 1))
    for index, number in enumerate(seq_gen):
        result += number
        if index % step == step - 1:
            yield result

You could even go one further step and make the sequence a parameter:

# iterates through a sequence, printing every step'th sum
def sequence_sums(sequence, step):
    result = 0
    for index, number in enumerate(sequence):
        result += number
        if index % step == step - 1:
            yield result

Called via:

sequence = (i if i % 2 else -i for i in range(1, 101))

for sum in sequence_sums(sequence, 10):
    print(sum)
\$\endgroup\$
  • 1
    \$\begingroup\$ Note the comparison change by starting at 0 => enumerate takes a second, optional, argument: start. So you can use enumerate(sequence, 1) and keep the usual comparison of the modulo to 0. \$\endgroup\$ – Mathias Ettinger Dec 11 '18 at 8:00
1
\$\begingroup\$

The other answers are fine. Here is a mathematical analysis of the problem you're trying to solve.

If this code is, for some reason, used in a performance-critical scenario, you can calculate the sum to \$m\$ in \$O(1)\$ time.

Notice that:

$$ \begin{align} 1-2+3-4+5-6\dots m &= -\sum_{n=1}^{m}n(-1)^n \\ &= \sum_{n=1}^{m}n(-1)^{n-1} \\ &= \frac{1}{4}-\frac{1}{4}(-1)^m(2m+1) \; ^* \end{align} $$

* There was a typo in my original comment.

Because you only want to see every 10th result, we can substitute \$m=10u\$ where \$u\in\mathbb{Z}\$. This is fortunate because for all integers \$(-1)^{10u} \equiv 1\$. Therefore:

$$ \begin{align} \frac{1}{4}-\frac{1}{4}(-1)^{10u}(20u+1) &= \frac{1}{4}-\frac{1}{4}(20u+1) \\ &= \frac{1}{4}-\frac{20u+1}{4}\\ &= \frac{(1-1)-20u}{4} \\ &= -5u \end{align} $$

Look familiar? It results in \$-5\$, \$-10\$, \$-15\$, ...

This fact is obvious from the output, but now knowing the series that produces it, we can calculate the final result for any such \$m\$ quickly, and every 10th value even easier.

We can avoid computing the exponent \$(-1)^m\$ because \$(-1)^{m} = 1\$ for even values of \$m\$ and \$-1\$ for odd values.

I'm not as familiar with Python, but here's an example:

def series(m):
    alt = 1 if m % 2 == 0 else -1
    return int(1/4 - 1/4 * alt * (2 * m + 1))

def series_progress(m):
    return -5 * m

m = 134

for i in range(1, m // 10):
    print(series_progress(i))

print(series(m))

This avoids the costly computation for the final result. If we just needed the result it would be \$O(1)\$, but because we give "progress reports" it is more like \$\lfloor\frac{n}{10}\rfloor\in O(n)\$.

\$\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.