4
\$\begingroup\$

Context:

I'm coding up a simple genetic optimization algorithm from first principles, and have decided to use boolean/binary strings as genes so that I can flip bits on or off as a form of mutation. In order to evaluate the fitness of the solutions that it generates I need to be able to decode the binary strings produced into the integer values that I'm optimizing for. The genetic optimization technique that I'm using returns it's individuals as a list, so I am extracting the integers as follows:

Problem:

Take the boolean/binary list...

l = [1, 0, 1, 0, 0, 1, 1, 1, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 1, 1, 0, 1, 1]

...split it into 8-bit sublists...

[[1, 0, 1, 0, 0, 1, 1, 1], [0, 0, 1, 0, 1, 0, 1, 0], [0, 0, 1, 1, 1, 0, 1, 1]]

...concatenate each sublist into a string...

[10100111, 101010, 111011]

...convert those strings from binary to decimal...

[167, 42, 59]

...then scale those integers between 50 and 150...

[115, 66, 73]

My solution:

[int(int(str(bin), 2)/2.55)+50 for bin in [int(''.join(\
    map(str,num))) for num in [l[i:i +8] for i in range(0, len(l), 8)]]]

This feels like a very ugly way to go tackle the problem - can anyone come up with a nicer way to go about it?

\$\endgroup\$
  • \$\begingroup\$ you need to explain to us how you are implementing this solution, there is not enough context surrounding your code. \$\endgroup\$ – Malachi Oct 14 '18 at 19:06
  • \$\begingroup\$ Please let me know if this is more appropriate @Malachi \$\endgroup\$ – Ari Cooper-Davis Oct 15 '18 at 15:55
6
\$\begingroup\$

The individual steps that you describe in the Problem section are each simple and easy to follow. Yet you write your code in basically a single line. To understand it I had to tear it apart like you did in your "explanation for humans":

scaled = [
    int(int(str(bin), 2)/2.55)+50
        for bin in [
            int(''.join(map(str, num)))
                for num in [
                    l[i : i+8] for i in
                        range(0, len(l), 8)]]]

I have also reformatted the code to better see the underlying structure. Together with your explanation, this works for understanding it.

But since you chose to not include the explanation within your code, I would have no chance to understand it by reading it alone.

Therefore, I find it easier to understand when you name the intermediate steps:

chunks = [l[i : i+8] for i in range(0, len(l), 8)]
binaries = [int(''.join(map(str, chunk))) for chunk in chunks]
numbers = [int(str(binary), 2) for binary in binaries]
scaled = [round(50 + (150 - 50) * number / (255 - 0)) for number in numbers]

There is a pythonic shortcut for step 1, and steps 2 and 3 can be combined into one:

chunks = zip(*[iter(l)]*8)
numbers = [sum(bit << shift for bit, shift in zip(chunk, reversed(range(8))) for chunk in chunks]
scaled = [round(50 + (150 - 50) * number / (255 - 0)) for number in numbers]

The aim of PEP8 is to ensure readable code. Having too many different ideas in a single line of code does not count as readable to me. Therefore I prefer the form with the intermediate steps.

Sure, the broken down version of the code is longer, but the reader of your code can take a deep breath after each step and inspect the intermediate result, just as you did in your explanation. Therefore the code should look this way.

By the way, I replaced the very last int with round since I think it is more appropriate. Decide for yourself. I also replaced the magic number 2.55 with the numbers from your explanation. This makes the numbers less magic.

\$\endgroup\$
  • \$\begingroup\$ Many thanks Roland, this is exactly what I needed to hear. I find it so easy to get caught up in reducing code down to simplify it that I make it less simple than it would have been to start with! Excellent suggestion replacing 'int' with 'round', and by making it more obvious where that number came from. \$\endgroup\$ – Ari Cooper-Davis Oct 14 '18 at 14:12
  • 2
    \$\begingroup\$ Good answer. A couple of improvements: (i) chunks = zip(*[iter(l)]*8) — this idiom for splitting iterables into fixed-size groups is in the documentation for zip; (ii) numbers = [sum(bit << shift for bit, shift in zip(chunk, reversed(range(8))) for chunk in chunks] would avoid the need to convert to strings and back again. \$\endgroup\$ – Gareth Rees Oct 14 '18 at 14:31
  • \$\begingroup\$ @GarethRees Thanks for your improvements. I incorporated them, and the code feels less like a brick of text now, since it is only three lines long. :) \$\endgroup\$ – Roland Illig Oct 14 '18 at 19:53

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.