Given:
G
is an arbitrary graphpartitions
is a list of its edges divided into (in this case) 3 sets.
Color generation:
- Its purpose is to generate one distinct color for each partition.
- The function turns a number between 0 and 1 into an RGB color.
- For the given example, with 3 partitions, it would be called with 1/3, 2/3, and 1, and return a list of codes for green, blue, and red.
- For other numbers it would be used to spread the different colors as widely as possible.
- Visually, think of a circle with R, G, and B equally spaced around it. The number is the fraction of the way around the circle from R to pick the color, so if the number is .4, the color would be between G and B, closer to the G.
Color assignment:
- For each set in the partition, a different color is generated and assigned to a list of colors.
- In this case
edge_color_list
would be 9 greens, 4 blues, and 2 reds. - The actual list is: [(0, 1.0, 0.0), (0.0, 0, 1.0), (0.0, 0, 1.0), (0, 1.0, 0.0), (0, 1.0, 0.0), (0, 1.0, 0.0), (0, 1.0, 0.0), (0, 1.0, 0.0), (0, 1.0, 0.0), (0, 1.0, 0.0), (0.0, 0, 1.0), (0, 1.0, 0.0), (1.0, 0, 0.0), (1.0, 0, 0.0), (0.0, 0, 1.0)]
Problem:
The top and bottom sections can't really be changed, but the get_color()
function and edge_color_list
sections each look like they were written in C, and I think they could be done more elegantly.
It's obvious how I was thinking while writing it (i.e., how I would write it in C), but I'd like to know how Python coders think.
I'm having trouble writing Python without a heavy C accent (I wrote C for decades).
I understand the Python language fairly well (or know how to recognize what I don't know and how to look it up).
And I've reached the stage where what I've written feels wrong.
But, I still don't seem to think in native Python:
#!/usr/bin/env python3
import matplotlib.pyplot as plot
import networkx as nx
G = nx.petersen_graph()
partitions = [{0, 3, 4, 5, 6, 7, 8, 9, 11}, {1, 2, 10, 14}, {12, 13}]
def get_color(fraction):
if fraction < 1/3:
color = (1-3*fraction, 3*fraction, 0)
elif fraction < 2/3:
fraction -= 1/3
color = (0, 1-3*fraction, 3*fraction)
else:
fraction -= 2/3
color = (3*fraction, 0, 1-3*fraction)
return color
edge_color_list = [None] * len(G.edges())
for i in range(len(partitions)):
items = list(partitions[i]) # convert from set
for j in range(len(items)):
edge_color_list[items[j]] = get_color((i+1)/len(partitions))
nx.draw(G, with_labels=True, pos=nx.circular_layout(G), width=2,
node_color='pink', edge_color=edge_color_list)
plot.show()
Any suggestions about how I could have thought differently while writing the above would be appreciated.
3 Answers 3
The get_color(fraction)
function might be better expressed using HSV representation of colour. I think it's meant to be an approximation of HSV(fraction, 1, 1). If so, you could use matplotlib.colors.hsv_to_rgb()
for conversion.
Iterating over a range(len(list))
is something of a Python anti-pattern. When possible, prefer to iterate over the elements themselves.
Instead of this:
for i in range(len(partitions)): items = list(partitions[i]) # convert from set for j in range(len(items)): edge_color_list[items[j]] = get_color((i+1)/len(partitions))
consider using the built-in iterate()
function to get pairs of indices and elements:
color_count = len(partitions)
for i, partition in iterate(partitions, start=1):
color = get_color(i/color_count)
for item in partition:
edge_color_list[item] = color
In the inner loop, I've removed the conversion from set to list, since we can iterate just as well over the set. That reduces unnecessary work your program is doing.
Another aspect of this refactoring is that hoisting the color
variable makes it more obvious that we're using a single colour for the set of edges; it also reduces work for the program, either at runtime or in the optimiser.
Overview
The code is easy to read and understand. Here are some minor suggestions.
DRY
This expression is repeated a couple times:
len(partitions)
Set it to a variable and use it:
num_partitions = len(partitions)
for i in range(num_partitions):
items = list(partitions[i]) # convert from set
for j in range(len(items)):
edge_color_list[items[j]] = get_color((i+1)/num_partitions)
The code might be more efficient since len
is not executed multiple
times in the nested loops.
Documentation
The PEP 8 style guide recommends adding docstrings for functions and also at the top of the code.
The header docstring should summarize the purpose of the code, similar to what you wrote in the question.
I would change this
for i in range(len(partitions)):
items = list(partitions[i]) # convert from set
for j in range(len(items)):
edge_color_list[items[j]] = get_color((i+1)/len(partitions)
to:
for idx, partition in enumerate(partitions): # I personally do it your way
items = list(partition) # convert from set
for item in items:
edge_color_list[items] = get_color((idx+1)/len(partitions)
# idx is the `i` from before
I referenced this.
There might be a linting plugin to help align your code towards being pythonic
-
\$\begingroup\$ feel free to edit/expand. I'm no expert... \$\endgroup\$Ali Pardhan– Ali Pardhan2020年08月07日 16:29:30 +00:00Commented Aug 7, 2020 at 16:29
-
\$\begingroup\$ This results in an error. Additionally how is this better than the OP's code? \$\endgroup\$2020年08月07日 16:29:40 +00:00Commented Aug 7, 2020 at 16:29
-
\$\begingroup\$ "feel free to edit/expand". The original title was "Writing python without a C accent" \$\endgroup\$Ali Pardhan– Ali Pardhan2020年08月08日 21:27:24 +00:00Commented Aug 8, 2020 at 21:27
-
\$\begingroup\$ furthermore,
enumerate
is the pythonic manner, whilerange(len(partitions))
is the C style \$\endgroup\$Ali Pardhan– Ali Pardhan2021年10月20日 07:27:35 +00:00Commented Oct 20, 2021 at 7:27