I am creating a database to store house points for our school running events. I would like to be able to tally all the points for the houses and it is working fine, however, I think it may be a bit verbose at this point.
Is there a simpler/better way with maybe more efficient code?
Races are stored in a races
table (competitorID, raceID, time).
Student data is stored in a competitor
table (competitorID, names, house).
The code will query the DB and bring back unique races, and then assign a point value based on their position (calculated by their race time).
import sqlite3
conn = sqlite3.connect('house.db')
conn.row_factory = sqlite3.Row
# House Totals
blueTotal = 0
greenTotal = 0
redTotal = 0
yellowTotal = 0
# Assign points from 1st = 15 through to 10-Last being 1
def points():
global pointvalue
if count == 0:
pointvalue = 15
places()
if count == 1:
pointvalue = 12
places()
if count == 2:
pointvalue = 10
places()
if count == 3:
pointvalue = 8
places()
if count == 4:
pointvalue = 7
places()
if count == 5:
pointvalue = 6
places()
if count == 6:
pointvalue = 5
places()
if count == 7:
pointvalue = 4
places()
if count == 8:
pointvalue = 3
places()
if count == 9:
pointvalue = 2
places()
if count > 10:
pointvalue = 1
places()
#Add points to houses
def places():
global blueTotal, greenTotal, redTotal, yellowTotal, pointvalue
if competitors['house'] == "blueTotal":
blueTotal += pointvalue
if competitors['house'] == "redTotal":
redTotal += pointvalue
if competitors['house'] == "yellowTotal":
yellowTotal += pointvalue
if competitors['house'] == "greenTotal":
greenTotal += pointvalue
for x in range(1,50):
competitorDetails = conn.execute('SELECT firstname, surname, house '
'FROM race, competitor '
'WHERE competitor.competitorID = race.competitorID '
'AND race.raceID = ' + str(x))
count=0
for competitors in competitorDetails:
points()
count += 1
print("blueTotal " + str(blueTotal))
print("greenTotal " + str(greenTotal))
print("redTotal " + str(redTotal))
print("yellowTotal " + str(yellowTotal))
3 Answers 3
DRY
This repetitive code is an anti-pattern:
blueTotal = 0
greenTotal = 0
redTotal = 0
yellowTotal = 0
Please define
houses = 'blue green red yellow'.split()
and then you can use array access to perform "the same action" across all houses:
for house in houses:
total[house] = 0
One could also assign total = collections.defaultdict(int)
, but that would
be a horse of a different color.
(https://docs.python.org/3/library/collections.html#collections.defaultdict)
global
The global
keyword is usually not your friend,
it leads to unfortunate coupling.
Here, it is offering you a hint that you want to define a class
,
set totals to zero in the __init__()
constructor,
and then have places()
access self.total[]
.
arg passing
This is a bit crazy:
for competitors in competitorDetails:
points()
Yes, you can treat competitors
as an implicit argument
by making it global, but there is absolutely no reason to,
and the current code makes it extremely difficult for readers
to understand what is going on. Please, please make competitors
an explicit argument, passing it in with points(competitors)
.
formatting
Clearly this works:
print("blueTotal " + str(blueTotal))
but the explicit call to str
is slightly verbose.
Consider re-phrasing such print statements in one of these ways:
print("blueTotal", blueTotal)
print(f'blueTotal {blueTotal}')
-
\$\begingroup\$ Don't you mean "a house of a different color"? ;-) \$\endgroup\$aghast– aghast2019年07月13日 14:07:43 +00:00Commented Jul 13, 2019 at 14:07
-
\$\begingroup\$ It's an idiom that, among other places, shows up in Oz. It means "quite a different thing", and implicit init to zero for all potential houses seemed quite different from OP's repetitive code. idioms.thefreedictionary.com/a+horse+of+a+different+color , oz.fandom.com/wiki/Horse_of_a_Different_Color \$\endgroup\$J_H– J_H2019年07月13日 14:24:24 +00:00Commented Jul 13, 2019 at 14:24
-
1\$\begingroup\$ @J_H ... Woosh. \$\endgroup\$DoctorDep– DoctorDep2019年07月14日 16:27:58 +00:00Commented Jul 14, 2019 at 16:27
-
\$\begingroup\$ Why did you choose to say
houses = 'blue green red yellow'.split()
instead ofhouses = ['blue', 'green', 'red', 'yellow']
? The second one seems more explicit to me... \$\endgroup\$Alex F– Alex F2019年07月14日 18:21:27 +00:00Commented Jul 14, 2019 at 18:21 -
\$\begingroup\$ It's a concise and common idiom. For N items, I can choose to put N blanks between them, or 4*N punctuation characters, which can mean the difference between a one-line or multi-line expression. As N gets bigger, so does the relative advantage. \$\endgroup\$J_H– J_H2019年07月14日 18:28:36 +00:00Commented Jul 14, 2019 at 18:28
Here are some hints on how to decrease the size of your code and make it more Pythonic.
Stack Ifs
Anytime you see stacked ifs, that are all basically the same, you should consider using a dict
. In the case here, the primary thing the ifs were doing was mapping a race place to points. So let's do that explicitly like:
pointvalues = {
0: 15,
1: 12,
2: 10,
3: 8,
4: 7,
5: 6,
6: 5,
7: 4,
8: 3,
9: 2,
10: 1,
}
Use a dict
for tracking like things
Instead of the specially named globals variables for tracking points, you can use a dict, and use your house names as keys to the dict
like:
# House Totals
totals = dict(
blueTotal=0,
greenTotal=0,
redTotal=0,
yellowTotal=0,
)
Or you might use setdefault
to save having to init the houses names at all.
Use enumerate
:
Instead of explicitly counting loop iterations, you should use the builtin enumerate
like:
for place, competitors in enumerate(competitorDetails):
# Add points to houses
totals[competitors['house']] += pointvalues.get(place, 0)
Also the above shows how to use the two dict
's we built earlier.
-
2\$\begingroup\$ A
dict
with consecutive integer keys starting at0
is isomorphic to alist
. \$\endgroup\$Jörg W Mittag– Jörg W Mittag2019年07月13日 21:49:00 +00:00Commented Jul 13, 2019 at 21:49
A few things no one has touched on yet...
points()
Your points values follow a pattern which can be easily modeled mathematically:
# Assign points from 1st = 15 through to 10-Last being 1
def points():
global pointvalue
if count == 0:
point_value = 15
places()
elif count < 3:
point_value = 14 - 2 * count
places()
elif count < 11:
point_value = 11 - count
places()
elif count > 10:
point_value = 1
places()
On my machine, this improvement made this section of the program about 2x faster.
if-elif
Since competitors['house']
can only have one value, there is no sense in checking if it is all values with repetitive if
statements. Instead, use an if-elif
structure or perhaps if-elif-elif-else
.
# add points to houses
def places():
global blue_total, green_total, red_total, yellow_total, point_value
if competitors['house'] == "blue_total":
blue_total += point_value
elif competitors['house'] == "red_total":
red_total += point_value
elif competitors['house'] == "yellow_total":
yellow_total += point_value
elif competitors['house'] == "green_total":
green_total += point_value
Style:
Python's own style guide dictates a convention for function and variable names: names should be lowercase, with words separated by underscores as necessary to improve readability.
Therefore: blueTotal
-> blue_total
and pointvalue
-> point_value
.
time
being used, as you described in your preliminary text. You should update your SQL to include this. Also, much of this work can be done in SQL rather than Python. Can you explain why you're using Python for this instead? \$\endgroup\$