7
\$\begingroup\$

I am attempting to generate a new colour and its contrasting colour with this python script.

I plan to try to make a web app that is different every time it is loaded (i.e. background and text colour)

I am wondering if there is a better and more efficient way to do this?

Here is my code:

import random

values = ['0','1','2','3','4','5','6','7','8','9','10','11','12','13','14','15']
value_dict = [['A','10'],['B','11'],['C','12'],['D','13'],['E','14'],['F','15']]
start = "#"

def contrastcolour(colour):
    if colour[0] == '#':
        colour = colour[1:]
    rgb = (colour[0:2], colour[2:4], colour[4:6])
    comp = ['%02X' % (255 - int(a, 16)) for a in rgb]
    return ''.join(comp)

def startcolour():
    colour = "#"
    for i in range(6):
        x = values[random.randint(0,len(values)-1)]

        for thing in value_dict:
            if x == thing[1]:
                x = thing[0]
        colour = colour + x
    return colour

base = startcolour()
contrast = start + contrastcolour(base)

print("The colours: {0}".format([base, contrast]))

The sample output is:

The colours: ['#2EF7F3', '#D1080C']
\$\endgroup\$
  • \$\begingroup\$ It's a specification issue rather than a code issue, but it seems to me that you need a better contrasting-color algorithm. What contrasting color does your code generate for a medium grey (#7F7F7F)? \$\endgroup\$ – Mark Dec 8 '17 at 1:14
11
\$\begingroup\$

Review

  • Globals should be ALL_CAPS

    start
    

    => START

  • Accumulation: Loop variables that are not explicitly used within the loop block should be replaced by _ , as is standard Python style.

    for i in range(6):
    

    You don't use the i later on, so the convention is to use _ for a dummy variable.

    => for _ in range(6)

  • The name of the variable value_dict implies it's a dict (Python's built-in dictionary type) but it's implemented as a list.

    value_dict = [['A','10'],['B','11'],['C','12'],['D','13'],['E','14'],['F','15']]
    ...
    for thing in value_dict:
        if x == thing[1]:
            x = thing[0]
    

    You could change this to use an actual dict like this:

    val_dict = {'10' : 'A', '11' : 'B', '12' : 'C', '13' : 'D', '14' : 'E', '15' : 'F'}
    ...
    
    if x in val_dict:
        x = val_dict[x]
    
  • Or better yet why not immediately convert to an hexadecimal using hex?

  • Check your naming. Don't be overly generic.

    values #
    thing  # Each of these could literally be anything
    x      #
    
  • colour = colour + x can be rewritten as colour += x

  • Use spaces around the comma: a,b => a, b

  • Use a if __name__ == '__main__' guard


Essentially your startcolour() can be rewritten as a one-liner

from random import randint
_HEX = list('0123456789ABCDEF')
...
def startcolour():
    return '#' + ''.join(_HEX[randint(0, len(_HEX)-1)] for _ in range(6))
\$\endgroup\$
  • \$\begingroup\$ "Variables that are not used in loops should be replaced by _ as is Python idiom" I read this several times, and it wasn't until I read your code that I was able to come up with a guess as to what it's supposed to mean. Do you mean "Loop variables that are not explicitly used within the loop block should be replaced by _ , as is Python idiom"? \$\endgroup\$ – Acccumulation Dec 7 '17 at 17:11
  • \$\begingroup\$ What's the purpose of the trailing underscore at the end of the variable HEX_? \$\endgroup\$ – Peter Olson Dec 7 '17 at 17:25
  • \$\begingroup\$ @PeterOlson making sure you don't conflict with anything in the namespace already called HEX \$\endgroup\$ – Snowbody Dec 7 '17 at 18:17
  • 2
    \$\begingroup\$ list(...) call is unnecessary as you can already index and iterate strings. You could also just use random.choice(_HEX) for _ in range(6) \$\endgroup\$ – Joshua Griffiths Dec 7 '17 at 18:41
  • 1
    \$\begingroup\$ @JoshuaGriffiths This has been covered by another answer \$\endgroup\$ – Ludisposed Dec 7 '17 at 18:42
6
\$\begingroup\$

To expand on Ludisposed's answer, you could instead use random.choice, leaving you with:

_HEX = '0123456789ABCDEF'
def startcolour():
    return '#' + ''.join(random.choice(_HEX) for _ in range(6))

I however would suggest you make a Colour class, if you actually want to mutate colours. There are three values in a colour being red, blue and green. And so if you make a list that contains these three values, then you can focus on just that list, and not have to mutate to and from string, list, tuple, iterable, iterator.

And so I would use:

import random


_START = '#'
class Colour:
    def __init__(self, value):
        value = list(value)
        if len(value) != 3:
            raise ValueError('value must have a length of three')
        self._values = value

    def __str__(self):
        return _START + ''.join('{:02X}'.format(v) for v in self)

    def __iter__(self):
        return iter(self._values)

    def __getitem__(self, index):
        return self._values[index]

    def __setitem__(self, index):
        return self._values[index]

    @staticmethod
    def from_string(string):
        colour = iter(string)
        if string[0] == _START:
            next(colour, None)
        return Colour(int(''.join(v), 16) for v in zip(colour, colour))

    @staticmethod
    def random():
        return Colour(random.randrange(256) for _ in range(3))

    def contrast(self):
        return Colour(255 - v for v in self)


base = Colour.random()
print("The colours: {0}".format([str(base), str(base.contrast())]))
\$\endgroup\$
6
\$\begingroup\$

To make the code shorter, pick one random 6-digit hexadecimal number, rather than six random hexadecimal digits:

def start_colour():
    random_colour = random.randint(0, 0xffffff) # inclusive range
    return f'{START}{random_colour:06X}'
    # older pythons would use: return '{0}{1:06X}'.format(START,random_colour)

likewise:

def contrast_colour(colour):
    rgb = int(colour.lstrip(START), 16)
    complementary_colour = 0xffffff-rgb
    return f'{START}{complementary_colour:06X}'

The % method for string interpolation is out-of-date.

\$\endgroup\$
  • 2
    \$\begingroup\$ randCol is a name that doesn't conform to PEP-8. Similarly, your spacing is non-conforming. However, you came closer than anyone advising on random number generation (at least you don't use string concatenation for it). Still, I think it's really strange to write int('ffffff', 16), when you could write 0xffffff. \$\endgroup\$ – wvxvw Dec 7 '17 at 15:45
  • \$\begingroup\$ Be careful using a single integer for color. An RGB color is semantically a sequence of three integers, not a single value. This "contrast" operation works when treating color as a single number, but most other operations on colors will give bad results (e.g. interpolation). \$\endgroup\$ – starchild Dec 7 '17 at 20:23
  • \$\begingroup\$ @Snowbody It's still not PEP-8. I think you meant random_colour. \$\endgroup\$ – wizzwizz4 Dec 7 '17 at 22:22
  • \$\begingroup\$ I checked over PEP-8 and found a list of acceptable identifier formats; I didn't see that it recommended one over the others. Can you help me narrow it down? \$\endgroup\$ – Snowbody Dec 7 '17 at 22:41
  • \$\begingroup\$ @Snowbody You use camalCase while Python uses snake_case for variables and functions PEP8#Naming \$\endgroup\$ – Ludisposed Dec 8 '17 at 9:29
4
\$\begingroup\$

Unless you were explicitly tasked with generating numbers by concatenating strings, it doesn't really make sense to do it by concatenating strings.

Now, since you are operating on numbers, it makes sense for your functions to take numbers as input, not strings. This is in general a good practice: first convert the user's representation into your application's internal representation (sometimes this process is referred to as "normalization"), and then assume the format comfortable for your application.

Operations on colors, which are typically represented as a 24-bit integer, are one of the very few remaining areas of modern programming where low-level "bit magic" isn't frowned upon.

Below, is how'd I write it, using "bit magic" (not really magical).

import random
import functools


def start_color():
    return random.randint(0xffffff)


def constrast_color(color):
    r, g, b = color >> 16, color >> 8, color
    return functools.reduce(
        lambda a, b: (a << 8) | b,
        map(lambda x: 0xff - (x & 0xff), [r, g, b]),
    )

Test:

>>> '{:06X}'.format(constrast_color(0xffaa12))
'0055ED'
\$\endgroup\$
  • \$\begingroup\$ Did you consider simplifying your bit magic to '0xFFFFFF - color'? \$\endgroup\$ – Joshua Griffiths Dec 7 '17 at 18:30
  • \$\begingroup\$ +1 for operating on the color channels independently. It this case operating on the full color value works just as well, but in most cases it doesn't. \$\endgroup\$ – starchild Dec 7 '17 at 20:25

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.