and welcome to StackOverflow!
Super short, but with a dependency
If you can rely on external dependencies, take a look at the luhn
package. You can install it with pip install luhn
, and then your program becomes:
from luhn import generate
ID = input("Enter your first 8 digits of your id : ")
print(generate(ID))
Can't get much cleaner than this.
Changing your code
But let's suppose you don't want to use a third-party package. Let us first put your code in a function and format it:
def luhn(number):
return (
10 - sum(
map(
lambda a: a if a <= 9 else int(str(a)[0]) + int(str(a)[1]),
[
int(number[i])
if i % 2 == 0
else int(number[i]) * 2
for i in range(8)
]
)
) % 10
)
Step 1: list
to generator
You can simply replace your list comprehension with a generator expression to make it a bit faster since you don't need to build the whole list first:
def luhn(number):
return (
10 - sum(
map(
lambda a: a if a <= 9 else int(str(a)[0]) + int(str(a)[1]),
( # <--- changed this
int(number[i])
if i % 2 == 0
else int(number[i]) * 2
for i in range(8)
) # <--- changed this
)
) % 10
)
Step 2: Get rid of map
I personally don't like using map
when you have to write a lambda as well. It just kills the two purposes of map
: simpler code and fast, C-speed execution.
For instance, map(int, myseq)
is great, extremely fast and clean. But map(lambda x: int(x), myseq)
is cumbersome (you have to give a name x
to myseq
items) and slow (because lambdas don't benefit from the C-like speed of builtin functions). In this case, I would say that int(x) for x in myseq
reads much nicer.
Applying that to the previous function:
def luhn(number):
return (
10 - sum(
a if a <= 9 else int(str(a)[0]) + int(str(a)[1])
for a in (
int(number[i])
if i % 2 == 0
else int(number[i]) * 2
for i in range(8)
) # bonus: we have one less indentation level!
) % 10
)
Step 3: Get rid of one if-else
In the second for
loop, we have x if i % 2 == 0 else x * 2
where x = int(number[i])
. But since i % 2
is either 0
or 1
, you can benefit from this and write x * (1 + i%2)
. So our function becomes:
def luhn(number):
return (
10 - sum(
a if a <= 9 else int(str(a)[0]) + int(str(a)[1])
for a in (int(number[i]) * (1 + i%2) for i in range(8))
) % 10
)
Getting prettier, right?
Step 4: Split "even" and "odd" characters
Now think with me. When will i % 2
be equal to 0
and when will it be 1
? It is 0
when i
is even, and 1
when i
is odd. So, in the expression int(number[i]) * (1 + i%2)
, we will have int(number[i])
when i
is even, and int(number[i]) * 2
when i
is odd. Supposing that number
is 314159
, then the "even characters" (characters in even positions) 345
are returned as-is, and the "odd characters" 119
are multiplied by 2
.
Moreover, the expression a if a <= 9 else int(str(a)[0]) + int(str(a)[1])
is also trivial for even characters: since they are returned as-is, they obviously satisfy a <= 9
, and then they are also returned as-is.
Noting that even and odd characters can be obtained by slicing:
s = 314159
even = s[::2] # 345
odd = s[1::2] # 119
So we can split our function into two parts. It gets bigger now, but afterwards it'll get better:
def luhn(number):
evens = number[::2]
even_sum = sum(map(int, evens)) # sum even characters
odds = number[1::2]
odd_sum = sum(
a if a <= 9 else int(str(a)[0]) + int(str(a)[1])
for a in (int(odd) * 2 for odd in odds)
) # sum odd characters
return (
10 - (even_sum + odd_sum) % 10
)
Step 5: More refactoring
In "a if a <= 9 else int(str(a)[0]) + int(str(a)[1])
", what this piece of code is saying is: sum the digits of a
. Because, if a
has only one digit (a <= 9
) we are just returning it; otherwise, we are summing its first and second digits.
But then, taking a closer look, we are summing this sum of digits for all numbers yielded by str(int(odd) * 2) for odd in odds
! This means that we can first concatenate all the digits, and then sum them all:
def luhn(number):
evens = number[::2]
even_sum = sum(map(int, evens)) # sums even characters
odds = number[1::2]
multiplied_by_two = (str(int(odd) * 2) for odd in odds)
all_digits = ''.join(multiplied_by_two)
odd_sum = sum(map(int, all_digits))
return (
10 - (even_sum + odd_sum) % 10
)
And we finally merge the two sums into one:
def luhn(number):
evens = number[::2]
odds = number[1::2]
multiplied_by_two = (str(int(odd) * 2) for odd in odds)
all_digits = ''.join(multiplied_by_two)
total_sum = sum(map(int, evens + all_digits))
return (
10 - total_sum % 10
)
Now we just collapse into a single expression:
def luhn(number):
return (
10 - sum(
map(
int,
number[::2] + ''.join(
str(int(odd) * 2) for odd in number[1::2]
)
)
) % 10
)
Finally, we make it a one-liner and finish your program:
ID = input("Enter your first 8 digits of your id : ")
print(10 - sum(map(int, ID[::2] + ''.join(str(int(odd) * 2) for odd in ID[1::2]))) % 10)
Some metrics
By comparing our solutions:
import timeit
ID = '12345678'
print(timeit.timeit(lambda: 10 - sum(map(lambda a: a if a <= 9 else int(str(a)[0]) + int(str(a)[1]), [int(ID[i]) if i % 2
== 0 else int(ID[i]) * 2 for i in range(8)])) % 10, number=1000000))
print(timeit.timeit(lambda: 10 - sum(map(int, ID[::2] + ''.join(str(int(odd) * 2) for odd in ID[1::2]))) % 10, number=1000000))
you can see that the latter is ~30% faster.