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\$
1
  • \$\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, 2017 at 1:14

4 Answers 4

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\$
6
  • \$\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\$ Dec 7, 2017 at 17:11
  • \$\begingroup\$ What's the purpose of the trailing underscore at the end of the variable HEX_? \$\endgroup\$ Dec 7, 2017 at 17:25
  • \$\begingroup\$ @PeterOlson making sure you don't conflict with anything in the namespace already called HEX \$\endgroup\$
    – Snowbody
    Dec 7, 2017 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\$ Dec 7, 2017 at 18:41
  • 1
    \$\begingroup\$ @JoshuaGriffiths This has been covered by another answer \$\endgroup\$
    – Ludisposed
    Dec 7, 2017 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\$
7
  • 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, 2017 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, 2017 at 20:23
  • \$\begingroup\$ @Snowbody It's still not PEP-8. I think you meant random_colour. \$\endgroup\$
    – wizzwizz4
    Dec 7, 2017 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, 2017 at 22:41
  • \$\begingroup\$ @Snowbody You use camalCase while Python uses snake_case for variables and functions PEP8#Naming \$\endgroup\$
    – Ludisposed
    Dec 8, 2017 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\$
2
  • \$\begingroup\$ Did you consider simplifying your bit magic to '0xFFFFFF - color'? \$\endgroup\$ Dec 7, 2017 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, 2017 at 20:25

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.