0

I have a collection of Python objects, lets call them fruits. Some of these fruits might be faulty. To check for these faults, I have a number of functions that performs different checks, and returns a list of fruits that did not pass the check:

def not_delicious(fruits, limit):
    return fruit for fruit in fruits if fruit.tastyness < limit

def not_ripe(fruits):
    return fruit for fruit in fruits if not fruit.ripe

Now I want to have a method that lets me neatly log the result of these checks. A naive approach looks like this:

def log_not_delicious(fruits, limit):
    for fruit in not_delicious(fruits, limit):
        log("The fruit {name} was not delicious enough.".format(**fruit))

def log_not_ripe(fruits):
    for fruit in not_ripe(fruits):
        log("The fruit {name} was not ripe.".format(**fruit))

Thats a lot of boilerplate. To simplify, I would like a function log_check that takes the check I want to perform as input, and then do the logging for me. So I can use it like this:

log_check(not_delicious, fruits, 7)

Such a function should not be so hard to write. Something along these lines:

def log_check(check, fruit, *args, **kwargs):
    for fruit in check(fruit, *args, **kwargs):
        log(message.format(**fruit))

There is only one problem. How does log_check know what message to use? I have thought of two solutions to this, but I like none of them:

  1. Put it as a property on the check function, so I have e.g.:

    not_delicious.message = "The fruit {name} was not delicious enough."
    

    This doesn't feel very Pythonic - it feels more like JavaScript.

  2. Use a class instead as a function for check. This feels a bit overkill - I have been thought that I only need to use a class if I have an internal state. And there's no state here - I perform the check, and then I am done.

Is there a third alternative, or what is my best approach here?

  • 2
    The proper place for the message string is as an argument to log_check(). This has nothing to do with Python, it's a general principle that business logic should not be intermingled with I/O. – Kilian Foth May 15 '17 at 11:34
  • @KilianFoth I want to avoid having to pass it as a parameter each time the function is used. That way, the place invoking the logging does not need to keep track of the message. – Anders May 15 '17 at 11:54
  • @KilianFoth both 1 & 2 are ways to (transitively) have the message as an argument – Caleth May 15 '17 at 12:32
  • Explanations for down votes are always welcome. – Anders May 15 '17 at 16:52
3

This looks like a job for polymorphism.

You want to bind checks and messages together without binding them to the log_check code. This is a good idea.

You're reluctant to use a class because the class would be stateless. To that I say: Feh! Stateless objects are perfectly fine.

An object is a bag of functions that move around together. The state is just something that also changes the behavior of those functions together.

Stateless objects got a bad rep in OO because some goofballs thought stateless meant methods had to be static. No, that's backwards.

log_check( fruits = fruits, ripe(), delicious(7) )

Any reason it shouldn't be that simple?

3

If you e.g. don't want to rewrite a bunch of existing functions just to attach messages to them, you can do so externally:

CHECKS = {
  (check_ripeness, 'This ain\'t no ripe enough'),
  (check_taste, 'deliciousness does not conform to the standards'),
  # etc
}

# somewhere in your method
for (predicate, message) in CHECKS:
  if not predicate(fruit):
    log(message)

Using classes with a __call__ would achieve the same result, just less straightforwardly.

  • @Anders: updated to use tuples. You apparently don't need to easily look up a specific check by name. – 9000 May 15 '17 at 13:25
2

Create an type that holds the message and the checker function. Use namedtuple or a class.

from collections import namedtuple
Check = namedtuple('Check', ('message', 'checker'))

Now, you can create objects that hold both the message and a function to check the fruit.

ripe = Check(message = "is not ripe", lambda fruit: fruit.tastiness < 5)

Then you write a generic function

def log_fruits(fruits, checker):
    for fruit in fruits:
        if checker(fruit):
            print("Fruit: ", fruit, checker.message)
2

Use a class. Behavior is also part of the state if you are using polymorphism, but in this case you may not even need "formal" polymorphism - just use one class for all checks, with the message and predicate as parameters.

Additionally:

  • Since you are using a class, it may be a good idea to pass a predicate that checks a single fruit let the class handle the checking of a list of them.
  • To make the syntax nicer, you can use a decorator.

So, I would have done something like this:

log = print


class Fruit(object):
    def __init__(self, name, **kwargs):
        self.name = name
        for k, v in kwargs.items():
            setattr(self, k, v)

# Single class, receives predicate as parameter
class Check(object):
    def __init__(self, predicate, message):
        self.predicate = predicate
        self.message = message

    def check(self, fruits, *args, **kwargs):
        """ Run the predicate on a list of checks """
        for fruit in fruits:
            if self.predicate(fruit, *args, **kwargs):
                yield fruit

    def log(self, fruits, *args, **kwargs):
        """ Check a list of fruits, logging the results """
        for fruit in self.check(fruits, *args, **kwargs):
            log(self.message.format(**fruit.__dict__))


def check(message):
    """ Helper decorator to ease the creationg of Check objects """
    def wrapper(func):
        return Check(func, message)
    return wrapper


@check(message="The fruit {name} was not delicious enough.")
def not_delicious(fruit, limit):
    return fruit.tastyness < limit


@check(message="The fruit {name} was not ripe")
def not_ripe(fruit):
    return not fruit.ripe


fruits = [Fruit("Yellow Banana", ripe=True, tastyness=10),
          Fruit("Green Banana", ripe=False, tastyness=0),
          Fruit("Grapefruit", ripe=True, tastyness=-float('inf'))]


not_delicious.log(fruits, limit=7)
not_ripe.log(fruits)

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.