1

How to combine two functions together

I have a class controlling some hardware:

class Heater()
    def set_power(self,dutycycle, period)
       ...
    def turn_on(self)
       ...
    def turn_off(self)

And a class that connects to a database and handles all data logging functionality for an experiment:

class DataLogger()
    def __init__(self)
        # Record measurements and controls in a database
    def start(self,t)
        # Starts a new thread to acquire and record measuements every t seconds

Now, in my program recipe.py I want to do something like:

        log = DataLogger()

        @DataLogger_decorator
        H1 = Heater()

        log.start(60)

        H1.set_power(10,100)
        H1.turn_on()
        sleep(10)
        H1.turn_off()
        etc

Where all actions on H1 are recorded by the datalogger. I can change any of the classes involved, just looking for an elegant way to do this. Ideally the hardware functions remain separated from the database and DataLogger functions. And ideally the DataLogger is reusable for other controls and measurements.

Cœur
  • 37,241
  • 25
  • 195
  • 267
Bastiaan
  • 4,451
  • 4
  • 22
  • 33
  • 1
    Are you aware that `@decorator` is just really simple syntactic sugar, and what it desugars to? –  Jun 07 '12 at 22:27
  • Yes, halfway I guess, I have the impression that I still miss the deep tricks that can be done with it. Maybe the decorator thing isn't going to help at all. – Bastiaan Jun 12 '12 at 20:05

2 Answers2

2

For this scenario, I prefer using DataLogger as a BaseClass or Mixin for other classes rather than trying to do some sort of decorator magic (which doesn't really click for me as a pythonic way to use a decorator)

e.g.:

class DataLogger(object):
  def __init__(self):
      # do init stuff

  def startlog(self, t):
      # start the log 


class Heater(DataLogger):
   def __init__(self):
      # do some stuff before initing your dataLogger
      super(Heater, self).__init__() # init the DataLogger
   #other functions

that way you can just do:

h1 = Heater()
h1.startlog(5)
h1.do_other_stuff()

An example that uses it as a mixin for an existing class:

class DataLoggerMixin(object): 
  def __init__(self):
    # do your init things
    super(DataLogger, this).__init__()  # this will trigger the next __init__ call in the inheritance chain (i.e. whatever you mix it with)

class Heater(object):
    """ Here's a heater you have lying around that doesn't do data logging.  No need to change it."""

# add a new child class with 2 lines, that includes the DataLoggerMixin as the first parent class, and you will have a new class with all the DataLogging functionality and the Heater functionality. 
class LoggingHeater(DataLoggerMixin, Heater):
    """ Now its a data logging heater """
    pass  # no further code should be necessary if you list DataLoggerMixin first in the base classes. 


>>> logging_heater = LoggingHeater()
>>> logging_heater.start_log(5)
>>> logging_heater.do_heater_stuff()

The key with successfully using mixins in python is to understand how the method resolution order (MRO), particularly for super, works in a multiple inheritance situation. See this on cooperative multiple inheritance.

____________________________________________________________________

Alternative Method: Use a Wrapper Class

If Mixin methodology doesn't work for your scheme, another option would be to use DataLogger as a wrapper class for objects to be logged. Basically Data Logger would accept an object to do logging on in its constructor like so:

class DataLogger(object)
  def __init__(self, object_to_log)
    self.object = object_to_log   # now you have access to self.object in all your methods.
    # Record measurements and controls in a database
  def start(self,t)
    # Starts a new thread to aqcuire and reccord measuements every t secconds

I'm not sure what type of logging or monitoring is done and whether you need access to the object you're logging or if it is independent. If the former, presumably Heater, Valve, etc. all implement the same functions that DataLogger cares about so you can Log for them regardless of what class they are. (This is a handy core feature of dynamic languages like Python called "Duck typing", where you can operate on different types, as long as the types implement the functions or attributes you care about. "if it quacks like a duck . . .")

Your code might look more like this, using wrapper class methodology:

h1 = Heater() 
log = DataLogger(h1)
log.start(60)
h1.set_power(10,100)
h1.turn_on()
sleep(10)
h1.turn_off()

Hope this helps!

Community
  • 1
  • 1
B Robster
  • 40,605
  • 21
  • 89
  • 122
  • Thanks for your help. In addition we have the classes: Valve(), Temperature(), Pressure() and possibly more. I think it would be better if I don't have to change Logger and Heater when I add those. Looks like "Mixin" is a keyword for me to further study, thanks! – Bastiaan Jun 12 '12 at 20:18
  • A Mixin is just another word for a class that can be used as a supplementary base class to add additional functionality to other classes. By definition, Mixins implicate a multiple inheritence scheme. With a Mixin like the one I have above, you can add the functionality without changing the base classes (Valve, Temperature, Pressure, etc.) by creating a two-line Child class for each of them, e.g., LoggingHeater that includes the Datalogger mixin first in the inheritance list. – B Robster Jun 13 '12 at 15:50
  • Another option if the Mixin methodology i described above or the decorator methodology thg435 describes in his answer aren't a good fit, is to use DataLogger as a wrapper class for objects you want to log. This is probably the easiest to understand of the three methods. I've added code to demonstrate, above. I notice you're new to SO, make sure to mark an accepted answer sooner or later, for good karma, and so you'll be sure to get future answers to your questions. – B Robster Jun 13 '12 at 16:15
2

You can decorate the Heater and provide Logger as an argument to the decorator:

# define the primitive logger

class Logger(object):
    def log(self, func, args, kwargs):
        print "Logging %s %r %r" % (func, args, kwargs)


# define the decorator
# since it accepts an argument, it's essentially a decorator generator
# which is supposed to return the actual decorator
# which in turn adds a logger call to each method of the class

def with_logger(logger):

    def method_wrapper(cls):

        def wrap(name, fun):
            def _(self, *a, **kw):
                logger.log(name, a, kw)
                return fun(self, *a, **kw)
            return _

        for k in dir(cls):
            v = getattr(cls, k)
            if not k.startswith('__') and callable(v):
                setattr(cls, k, wrap(k, v))
        return cls

    return method_wrapper


# create a logger...

my_logger = Logger()

# ...and pass it to the decorator

@with_logger(my_logger)
class Heater(object):
    def set_power(self,dutycycle, period):
        pass
    def turn_on(self):
        pass
    def turn_off(self):
        pass

# let's test!

h = Heater()
h.set_power(100, 200)
h.turn_on()
h.turn_off()
georg
  • 211,518
  • 52
  • 313
  • 390
  • 1
    Thanks for your help. Would be nice if the choise for the logger is not hardcoded in the hardware specific class of the heater. – Bastiaan Jun 12 '12 at 20:22
  • @Bastiaan, it's not. Decorators are syntactic sugar and can be applied later, after class definition. E.g. if `Heater` was not decorated when it was defined, application code could create `my_logger` and alias `Heater = with_logger(my_logger)(Heater)`. Then all later `Heater` objects would have logging. – Jake Stevens-Haas Jun 03 '21 at 19:45