3
\$\begingroup\$

This problem can be found at LeetCode. Since I was studying idiomatic or 'pythonic' code in my language today, I decided to take the most 'pythonic' approach and challenged myself to do this in one line only.

LeetCode problem description :

  • You are given a large integer represented as an integer array digits, where each digits[i] is the ith digit of the integer. The digits are ordered from most significant to least significant in left-to-right order. The large integer does not contain any leading 0's. Increment the large integer by one and return the resulting array of digits.

My concern Is:

  • Is it okay to sacrifice speed and readability over 'pythonic' being of the code ?

Code:

class Solution:
    def plusOne(self, digits: List[int]) -> List[int]:
        return [int(number) for number in str((int("".join([str(number) for number in digits])) + 1))]
\$\endgroup\$
1
  • 4
    \$\begingroup\$ I would disagree that 'pythonic' means one-liners or similar. I consider 'pythonic' code to be code that enhances readability and often performance as well. \$\endgroup\$ Commented Sep 1, 2023 at 18:47

3 Answers 3

6
\$\begingroup\$

Python implementations may throw a ValueError when converting large strings to int. In any case, converting to binary and back is likely to cost a lot of performance for this challenge.

A better approach is to add with carry the way you would with pencil and paper: add one to the least significant digit; if the result is 10, then replace it with zero and add one to the next digit, repeating until there is no carry. If you run out of digits, prepend an extra one.

Unless you're competing in code golf (which would make your code off-topic here), then brevity should not be your main goal. Write for clarity and maintainability instead.

For future reviews, I recommend you include your unit-tests along with the code itself. That helps show what you know to work, and we might be able to identify edge-cases that you have missed.

\$\endgroup\$
5
\$\begingroup\$

As mentioned by Toby, this is the wrong way to solve this problem. It works, but if submitted as homework in a Computer Science class, it would score poorly. LeetCode may give you statistics on the time & memory usage by your solution compared to other submissions; look at those to determine why this is the wrong solution.

But it is working code, so let's review it anyway.

        return [int(number) for number in str((int("".join([str(number) for number in digits])) + 1))]

That's an awfully long line of code, which does a whole lot. You would do well to split it up into multiple statements:

        value_str = "".join([str(number) for number in digits])
        value = int(value_str)
        incremented = value + 1
        incremented_str = str(incremented)
        return [int(number) for number in incremented_str]

This is clearer. We are:

  1. turning the list of digits into a string of digits
  2. converting that to an integer
  3. adding one to that integer
  4. turning the incremented value into a string
  5. converting the string into a list of digits and returning that.

You can put break points in the code, or print statements to debug it at each step. Maintainable.

Let's make it even better.

        value_str = "".join([str(number) for number in digits])

Here, you are constructing a new list (of the string values of each of the digits) as an intermediate value. This is a waste of time and space. The str.join() function takes any iterable object. Your list is - of course - iterable, but so are generators.

        value_str = "".join(str(number) for number in digits)

Two character shorter, and no intermediate list needed to be constructed.

This pattern can be seen again and again: applying a function (str) to every item (number) in an iterable (digits), resulting in a new iterable. It is so common, Python has a function for this: map(func, iterable):

        value_str = "".join(map(str, digits))

Well, obviously if we can do this for the first statement, we can do it for the last one too.

        return [int(number) for number in incremented_str]

becomes

        return list(map(int, incremented_str))

So our function body is now:

        value_str = "".join(map(str, digits))
        value = int(value_str)
        incremented = value + 1
        incremented_str = str(incremented)
        return list(map(int, incremented_str))

If you wanted to one-line this, it is shorter and faster than your original.

class Solution:
    def plusOne(self, digits: List[int]) -> List[int]:
        return list(map(int, str(int("".join(map(str, digits)))+1)))

But ... What is that self argument all about? We never use it. Let's get rid of it too!

class Solution:
    @staticmethod
    def plusOne(digits: List[int]) -> List[int]:
        return list(map(int, str(int("".join(map(str, digits)))+1)))

Again, this is still the wrong way to solve this problem.

Another way that might be more Pythonic

janos suggested a way using generators as being more Pythonic. Using itertools to count the number of trailing 9's to get the digit to increment, and then simple list slicing/splicing, would be yet another way:

from itertools import takewhile
from more_itertools import ilen

class Solution:
    @staticmethod
    def plusOne(digits: list[int]) -> list[int]:
        num_digits = len(digits)
        nines = ilen(takewhile(lambda digit: digit == 9, reversed(digits)))

        if num_digits > nines:
            inc_digit = num_digits - nines - 1
            return digits[:inc_digit] + [digits[inc_digit] + 1] + [0] * nines
        else:
            return [1] + [0] * nines
\$\endgroup\$
4
\$\begingroup\$

PEP 20 – The Zen of Python

To write Pythonic code, I think PEP 20 – The Zen of Python is the great place to start.

I think the posted code doesn't do great with respect to these points:

  • Beautiful is better than ugly.
  • Sparse is better than dense.
  • Readability counts.
  • If the implementation is hard to explain, it's a bad idea.
  • If the implementation is easy to explain, it may be a good idea.

Another way that might be more Pythonic

What do we need here. Maybe we can grow a function out of it in step by step.

Definitely go over digits from the end, so an iterator in reverse order: reversed(digits)

And we want to do something for each digit:

it = reversed(digits)
for value in it:
    ...

If adding 1 would overflow, then the current digit is 0. And we need to continue onto the next digit. Yielding 0 now would have exactly that effect:

for value in it:
    if value + 1 == 10:
        yield 0

The loop will continue as long as +1 would overflow, which will happen when the number ends with consecutive 9s.

Ok at some point we'll reach a digit that's not a 9. At that point the current digit is value + 1. And then we don't want to continue the loop, because the remaining digits must stay as they are. So we can yield them all, and get out of the loop:

    else:
        yield value + 1
        yield from it
        return

Ok so with yield we are now clearly building a generator function, but we haven't spelled it out yet, let's just do that now:

def helper(it):
    for value in it:
        if value + 1 == 10:
            yield 0
        else:
            yield value + 1
            yield from it
            return

Well what if we reached the end of the loop and didn't return? This will be the case when all the digits were 9. We did yield 0 for all of them. What's missing is a yield 1 at the end.

We just need to call this function, and reverse the digits so that they are in the expected order:

def helper(it):
    ...
    
return list(reversed(list(helper(reversed(digits)))))

As @AJNeufeld pointed out, we can save the last list allocation by reversing items in-place:

result = list(helper(reversed(digits)))
result.reverse()
return result

Putting it together:

class Solution:
    def plusOne(self, digits: List[int]) -> List[int]:
        def helper(it):
            for value in it:
                if value + 1 == 10:
                    yield 0
                else:
                    yield value + 1
                    yield from it
                    return

            yield 1
            
        result = list(helper(reversed(digits)))
        result.reverse()
        return result
\$\endgroup\$
1
  • 1
    \$\begingroup\$ Might it be more performant to write value == 9 rather than value + 1 == 10? I can see a clarity advantage to the latter, though. \$\endgroup\$ Commented Sep 22, 2023 at 11:02

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.