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\$
7
  • \$\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
    Commented Dec 10, 2018 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
    Commented Dec 11, 2018 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
    Commented Dec 11, 2018 at 1:50
  • \$\begingroup\$ @esote this seems like the start of an answer \$\endgroup\$ Commented Dec 11, 2018 at 21:59
  • 1
    \$\begingroup\$ @esote Mathematical analysis of a problem in order to reduce code complexity are usually well received. \$\endgroup\$ Commented Dec 11, 2018 at 22:20

5 Answers 5

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\$
2
  • \$\begingroup\$ I would recommend writing expressions with a sign variable as sign * value to match the standard method of writing numbers. \$\endgroup\$ Commented Dec 10, 2018 at 18:41
  • \$\begingroup\$ @Apollys Right, fixed. \$\endgroup\$ Commented Dec 10, 2018 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\$
2
  • \$\begingroup\$ @MathiasEttinger: It seems like MaartenFabre is already going in that direction in their answer. \$\endgroup\$
    – Graipher
    Commented Dec 10, 2018 at 15:25
  • \$\begingroup\$ Yep, saw that after the comment \$\endgroup\$ Commented Dec 10, 2018 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\$
3
  • \$\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
    Commented Dec 10, 2018 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\$ Commented Dec 10, 2018 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\$ Commented Dec 11, 2018 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
  • 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\$ Commented Dec 11, 2018 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 and acknowledge you have read our privacy policy.

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