3
\$\begingroup\$

I have a function that is taking in a list where each element is a vote in JSON format. I am then building and returning a dictionary that looks like this:

{ "District A" : {
    "Match A" : {
        "Candidate A" : {
            0 : 10
            1 : 20
        }
     }
 }

Each element in the list could look something like this:

{
    "district": "district a",
    "list": {
        "matches": [
            {
                "match": "King",
                "list": {
                    "candidates": [
                        {
                            "match": "King",
                            "candidate": "Candidate 1",
                            "ranking": 0
                        },
                        {
                            "match": "King",
                            "candidate": "Candidate 2",
                            "ranking": 1
                        },
                        {
                            "match": "King",
                            "candidate": "Candidate 3",
                            "ranking": 2
                        },
                        {
                            "match": "King",
                            "candidate": "Candidate 4",
                            "ranking": 3
                        },
                        {
                            "match": "King",
                            "candidate": "Candidate 5",
                            "ranking": 4
                        }
                    ]
                }
            },
            {
                "match": "Queen",
                "list": {
                    "candidates": [
                        {
                            "match": "Queen",
                            "candidate": "Candidate 1",
                            "ranking": 2
                        },
                        {
                            "match": "Queen",
                            "candidate": "Candidate 2",
                            "ranking": 0
                        },
                        {
                            "match": "Queen",
                            "candidate": "Candidate 3",
                            "ranking": 0
                        },
                        {
                            "match": "Queen",
                            "candidate": "Candidate 4",
                            "ranking": 1
                        },
                        {
                            "match": "Queen",
                            "candidate": "Candidate 5",
                            "ranking": 0
                        }
                    ]
                }
            }
        ]
    }
}

Here is my code that works:

def collate_by_district(votes):
    collated_votes = {}    

    for vote in votes:
        # If the district is in collated_votes
        if vote['district'] in collated_votes:
            for bout in vote['content']['matches']:
                # If the matches is in collated_votes
                if bout['match'] in collated_votes[vote['district']]:
                    # Check if the candidate is in collated_votes
                    for candidate in bout['content']['candidates']:
                        # If the candidate is already in collated_votes update their ranking
                        if candidate['candidate'] in collated_votes[vote['district']][bout['match']]:
                            if candidate['ranking'] in collated_votes[vote['district']][bout['match']][candidate['candidate']]:
                                collated_votes[vote['district']][bout['match']][candidate['candidate']][candidate['ranking']] += 1
                            else:
                                collated_votes[vote['district']][bout['match']][candidate['candidate']][candidate['ranking']] = 1
                        else:
                            rankings = {}
                            if candidate['ranking'] in rankings:
                                rankings[candidate['ranking']] += 1
                            else:
                                rankings[candidate['ranking']] = 1
                            collated_votes[vote['district']][bout['match']][candidate['candidate']] = rankings
                else:
                    match = {}
                    for candidate in bout['content']['candidates']:
                        rankings = {}
                        if candidate['ranking'] in rankings:
                            rankings[candidate['ranking']] += 1
                        else:
                            rankings[candidate['ranking']] = 1
                        match[candidate['candidate']] = rankings
                    collated_votes[vote['district']][bout['match']] = match
        else:
            match = {}
            for bout in vote['content']['matches']:
                candidates = {}
                for candidate in bout['content']['candidates']:
                    rankings = {}
                    candidates[candidate['candidate']] = rankings
                    if candidate['ranking'] in rankings:
                        rankings[candidate['ranking']] += 1
                    else:
                        rankings[candidate['ranking']] = 1
                match[bout['match']] = candidates
            collated_votes[vote['district']] = match    

    return collated_votes

It works, and is fast enough, but it just seems rather unwieldy and hard to read. Is there a better way that all these nested if/then statements?

\$\endgroup\$
  • \$\begingroup\$ Welcome to CodeReview.SE. Would you be able to provide a bit more explanation about what your function is supposed to do and how you call it ? \$\endgroup\$ – SylvainD Dec 8 '17 at 21:36
4
\$\begingroup\$

Is there a better way? Yes, absolutely!

There are two tools that ship standard with Python that you need to investigate: collections.Counter and collections.defaultdict.

For your code that looks like this:

if vote['district'] in collated_votes:
if bout['match'] in collated_votes[vote['district']]:
if candidate['candidate'] in collated_votes[vote['district']][bout['match']]:
if candidate['ranking'] in collated_votes[vote['district']][bout['match']][candidate['candidate']]:
else:
    match = {}
    rankings = {}

All this stuff is exactly what defaultdict is intended to handle: provide a dictionary that, when accessed with a key not currently present in the dictionary, returns a "default" value generated from a no-args factory function.

You can use this with dict as the factory function, or list or even something clever like a functools.partial of defaultdict that generates nested defaultdicts!

You can also pass int as the factory, which constructs an integer (default value = 0) as the default value.

But wait! Instead of using defaultdict(int) you could also use collections.Counter. This implements a bag that counts the number of times each element is added. (It's much like defaultdict(int) but with slightly different semantics.)

Which one you use (defaultdict(int) or Counter()) will depend on your code structure. If you can structure the items as a sequence or generator, the Counter might be the best option - it can take a constructor parameter that slurps them all up. If you have to bounce around from one collection to another because of the way your data is structured, the defaultdict approach might be best.

Something like this:

import functools

from collections import defaultdict

Candidate_factory = functools.partial(defaultdict, int)
Matches_factory = functools.partial(defaultdict, Candidate_factory)
Districts_factory = functools.partial(defaultdict, Matches_factory)

Districts = Districts_factory()

for district in ("District A",):
    for match in ("Match 1",):
        for candidate in ("Leroy",):
            Districts[district][match][candidate] += 1

import pprint
pprint.pprint(Districts)

Which outputs:

$ python test.py
defaultdict(functools.partial(<class 'collections.defaultdict'>, functools.partial(<class 'collections.defaultdict'>, <class 'int'>)),
            {'District A': defaultdict(functools.partial(<class 'collections.defaultdict'>, <class 'int'>),
                                       {'Match 1': defaultdict(<class 'int'>,
                                                               {'Leroy': 1})})})
\$\endgroup\$
  • \$\begingroup\$ Thanks for the help. I am going to read up on everything you provided over the weekend, but this looks like it will help me a lot. \$\endgroup\$ – chemical Dec 8 '17 at 23:07

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.