7

I have a script that takes a list of metrics as an input, and then fetches those metrics from the database to perform various operations with them.

My problem is that different clients get different subsets of the metrics, but I don't want to write a new IF block every time we add a new client. So right now, I have a large IF block that calls different functions based on whether the corresponding metric is in the list. What is the most elegant or Pythonic way of handling this?

Setup and function definitions:

clientOne = ['churn','penetration','bounce']
clientTwo = ['engagement','bounce']

def calcChurn(clientId):
    churn = cursor.execute(sql to get churn)
    [...]
    return churn

def calcEngagement(clientId):
    engagement = cursor.execute(sql to get engagement)
    [...]
    return engagement

Imagine three other functions in a similar format, so there is one function that corresponds to each unique metric. Now here is the block of code in the script that takes the list of metrics:

def scriptName(client, clientId):
    if churn in client:
        churn = calcChurn(clientId)
    if engagement in client:
        engagement = calcEngagement(clientId)
    if penetration in client:
    [...]
1
  • 4
    If you're worried about doing things the most Pythonic way possible, you probably also want to use PEP 8 style: calc_churn and calc_engagement, spaces after commas in lists, etc. Commented May 18, 2015 at 1:00

3 Answers 3

18

Generally, you'd create a mapping of names to functions and use that to calculate the stuff you want:

client_action_map = {
  'churn': calcChurn,
  'engagement': calcEngagement,
  ...
}

def scriptName(actions, clientId):
    results = {}
    for action in actions:
        results[action] = client_action_map[action](clientId)
    return results
Sign up to request clarification or add additional context in comments.

2 Comments

This worked perfectly - thank you! A couple of questions out of curiosity: 1. Why does results[action] store a new variable for each item in the list , but if I just use action it only stores one variable that keeps getting overwritten with each iteration of the loop? 2. What would I need to do differently if each function in the client_action_map took a different number of inputs?
I'm not sure that I completely understand the first question. The second question is certainly tricky. You might use *args and **kwargs to make it so that all of the actions can accept the same arguments and simply not use some of those arguments, or you might need to suck it up and write them all out...
3

You can create a class with static methods and use getattr to get the correct method. It's similar to what mgilson suggests but you essentially get the dict creation for free:

class Calculators:

    @staticmethod
    def calcChurn():
        print("called calcChurn")

    @staticmethod
    def calcEngagement():
        print("called calcEngagement")

    @staticmethod
    def calcPenetration():
        print("called calcPenetration")

stats = ["churn", "engagement", "penetration", "churn", "churn", "engagement", "undefined"]

def capitalise(str):
    return str[0].upper() + str[1:]

for stat in stats:
    try:
        getattr(Calculators, "calc" + capitalise(stat))()
    except AttributeError as e:
        print("Unknown statistic: " + stat)

called calcChurn
called calcEngagement
called calcPenetration
called calcChurn
called calcChurn
called calcEngagement
Unknown statistic: undefined

3 Comments

So then why not use a dict? You're creating unknown attributes.
Because this way you only have to implement the method and not worry about adding it to the dict. It's just a matter of style and preference.
Each way is different steps. A dict you can easily access things. Regardless, +1 as it's useful.
0

Perhaps it might make sense to encapsulate the required calls inside an object.

If it makes sense for your clients to be object and especially if many clients call the same set of functions to obtain metrics, then you could create a set of Client sub classes, which call a predefined set of the functions to obtain metrics.

It's a bit heavier than the mapping dict.

''' Stand alone functions for sql commands.
    These definitions however dont really do anything.
'''


def calc_churn(clientId):
    return 'result for calc_churn'


def calc_engagement(clientId):
    return 'result for calc_engagement'


''' Client base object '''


class Client(object):
    ''' Base object allows list of functions
    to be stored in client subclasses'''

    def __init__(self, id):
        self.id = id
        self.metrics = []
        self.args = []

    def add_metrics(self, metrics, *args):
        self.metrics.extend(metrics)
        self.args = args

    def execute_metrics(self):
        return {m.__name__: m(*self.args) for m in self.metrics}


''' Specific sub classes '''


class Client1(Client):

    def __init__(self, id):
        ''' define which methods are called for this class'''

        super(Client1, self).__init__(id)
        self.add_metrics([calc_churn], id)


class Client2(Client):

    def __init__(self, id):
        ''' define which methods are called for this class'''

        super(Client2, self).__init__(id)
        self.add_metrics([calc_churn, calc_engagement], id)


''' create client objects and  '''

c1 = Client1(1)
c2 = Client2(2)

for client in [c1, c2]:
    print client.execute_metrics()

The result you will get from execute_metrics is a dict mapping the function name to its results for that client.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.