4
\$\begingroup\$

I wanted a timer, rather like the Visual Basic object, and I wanted it with no cumulative error. And I wanted it flexible, so I wouldn't have to write another one. I'm very lazy BTW, so lazy that I will go to great lengths to avoid having to write something a second time. Actually, this fits quite well with the "write once, refer often" coding style. I liked the flexibility of the .config() setup style used for Tk controls, so wanted to implement that.

I spent a long time searching for timers, and there seemed to be about 3 basic types, I eventually settled on the threading.Timer repeat call model. I spent far too long battling with freezes in IDLE, before realizing that my timers appeared to run OK standalone, and that IDLE just didn't like threads (I've just installed IPython, and will see how that copes once I've learnt my way round it).

Then I tried to get a flexible configuration, and that's where code bloat seemed to set in. I've tried to manage it back again by making lists of my attributes, and reusing those lists wherever I need them, I hope this class could serve as a pattern for any future classes with the minimum of editing. I understand the duck typing principle of try rather than test, but I prefer to have errors caught at the time I try to configure something, rather than later at the time I try to use it. I've tried to be as duck-like in my tests as possible, hoping to get the best of both worlds. I am not suggesting my tests are bomb-proof yet, I will test them more thoroughly in due course. My test for valid integers isn't quite as smart as I'd want it yet (accept 4, 0x11, '4', '0x11', reject 3.142) (int() covers enough of that ground for the moment), but that is a simpler issue to be tackled later.

My concerns are:

  1. Before I use it as a pattern for other classes, have I done a reasonably pythonic job, or am I just kidding myself?

  2. There seem to be a lot of lines of support, and very little payload, could the same effect have been achieved more efficiently?

  3. In searching for timers, I've seen a lot of comments bemoaning the fact that Python libraries don't have a standard repeat timer. Is anybody going to run into trouble using this one?

import time, threading

class Pacer():
    """ A Pacer object can be configured at instantiation,
    using config(kwargs), or at start(kwargs)
    Call Pacer_obj.config() with no args to get a list of valid kwargs
    It calls func_tick every period, with non-cummulative error (if possible)
    Set max_ticks or max_overruns to zero to disable them
    """

    def __init__(self,**kwargs):
        self.zpint_keys=['max_overruns','max_ticks']
        self.pfloat_keys=['period']
        self.func_keys=['func_tick','func_done','func_over']
        self.private_keys=['N_ticks','N_overruns','t_next_tick']

        for key in self.zpint_keys:
            setattr(self,key,0)
        for key in self.pfloat_keys:
            setattr(self,key,1)
        for key in self.func_keys:
            setattr(self,key,None)            
        for key in self.private_keys:
            setattr(self,key,0)

        self.config(**kwargs)

    def spill(self):
        print
        for key in self.zpint_keys+self.pfloat_keys+self.func_keys+self.private_keys:
            print key, '=',getattr(self,key)
        print

    def start(self,**kwargs):
        self.config(**kwargs)

        self.N_ticks=0
        self.N_overruns=0
        self.t_next_tick=time.time()+self.period
        self.t=threading.Timer(self.period,self.tick)
        self.t.start()

    def tick(self):
        if self.func_tick:
            self.func_tick()
        else:
            print "you do realise you haven't defined a tick callback, don't you"
        self.N_ticks += 1
        self.t_next_tick += self.period
        # have we reached maximum number of ticks?
        if (self.N_ticks >= self.max_ticks) and (self.max_ticks != 0):
            if self.func_done:
                self.func_done()
            else:
                print 'quit on max ticks, no callback defined'
            return              # quit without scheduling another tick
        # OK, so still ticking
        # how long till next, with non-cummulative error
        time2wait=self.t_next_tick-time.time()
        if time2wait <= 0:      # damn, we've overrun
            time2wait=0         # set to least time possible
            self.N_overruns += 1    # how many has that been?
        if (self.N_overruns >= self.max_overruns) and (self.max_overruns != 0):
            if self.func_over:
                self.func_over()
            else:
                print 'quit on too many missed schedules, no callback defined'
            return              # quit without scheduling another tick
        # OK, so *still* ticking
        self.N_overruns=0         # reset the overrun counter
        self.t=threading.Timer(time2wait+0.001,self.tick)
        self.t.start()

    def stop(self):
        self.t.cancel()     # and really nothing else needs to happen here
                            # it stops the next tick from happening
                            # which stops everything else

    def config(self,**kwargs):

        if not kwargs:
            usage={'non-neg integers':self.zpint_keys,
                  'positive floats':self.pfloat_keys,
                  'callback functions':self.func_keys}
            return usage

        for key in self.zpint_keys:
            if key in kwargs:
                keyval=kwargs.pop(key)
                try:
                    val=int(keyval)
                    if val<0:
                        print 'parameter ',key, ' must be zero or positive'
                        break
                except:
                    print 'parameter ',key,' must be an integer'
                    break
                setattr(self,key,val)

        for key in self.pfloat_keys:
            if key in kwargs:
                keyval=kwargs.pop(key)
                try:
                    val=float(keyval)
                    if val <= 0:
                        print 'parameter ',key,' must be positive'
                        break
                except TypeError:
                    print 'parameter ',key,' must be a float'
                    break
                setattr(self,key,val)                

        for key in self.func_keys:
            if key in kwargs:
                keyval=kwargs.pop(key)
                if not callable(keyval):
                    print 'parameter ',key,' must be callable function'
                    break
                setattr(self,key,keyval)          

        if kwargs:
            print 'unknown parameter(s) were supplied to Pacer.config()'
            print kwargs     

if __name__=='__main__':

    def hello():
        print 'hello world'

    q=Pacer()
    q.spill()

    b=q.config()
    print b

    q.config(max_ticks=7.5)
    q.spill()

    q.config(interloper=3)
    q.config(func_tick=hello,max_ticks=3)
    q.spill()

    q.start()
    time.sleep(5)
    a=raw_input('press return to quit - ')
\$\endgroup\$
  • \$\begingroup\$ Add docstrings! \$\endgroup\$ – Daenyth Jun 24 '12 at 12:21
2
\$\begingroup\$

My first impression is that it's overengineered by Python standards. (Or maybe just by my standards, which are a little on the cowboy side.)

Some specific crits:

  • Don't use prints for error cases in the config; raise exceptions instead. For diagnostics that aren't necessarily show-stopping, use the logging module so that client code can decide how to handle them.
  • My initial thought was not to track a callback function for the overrun case, but raise an exception instead, but that's problematic with the threading.
  • For a programmer-facing object, I would never go to so much trouble in accepting different datatypes. A string is not a number and pretending otherwise is dangerous.
  • If you insist on retaining the type checking and/or coercion, I would, rather than maintaining three separate lists, maintain one list of (name,type) tuples, or possibly a dict of lists keyed by type. But really, self.period = float(period).
  • Personally, I feel that kwargs games are for when you're tunneling through Python to an existing API.

Returning usage from config is an interesting idea, useful in the interactive shell... but you still haven't quite explicitly documented the function's behavior in either the usage or the docstring, so a user still has to RTFS, and you've obfuscated the source with a lot of parameter management code that isn't doing useful work.

| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ Thanks. Over engineered, that's what I was afraid of. A criticism I often level at my co-workers (in contrast to the "necessary and sufficient" of logical proof) is that something is unnecessary and insufficient, ie it's a lot of extra work and hasn't acheived the objective. Exceptions and logging - well, I am only "aspiring level 2", not got my head round those yet. Can you point to a good howto on logging? Accepting strings - point well taken. I think I should just accept all parameter setting things and then either do one consistency check in start(), or just use them with try/except. \$\endgroup\$ – Neil Thomas Jun 24 '12 at 9:55
  • \$\begingroup\$ Your desire to do parameter checking at config time instead of start time is reasonable, though, and we could talk all day about what the right thing to do is the user asks for max_ticks = 7.5. I come from the C culture where if you ask for something funny, you'll get defensible efficient behavior if possible or a crash if not, hence I would cast to int and never look back. There's a tutorial for logging in the standard library: docs.python.org/howto/logging.html \$\endgroup\$ – Russell Borogove Jun 24 '12 at 19:05
  • \$\begingroup\$ Hmm, I was afraid you'd point me at the docs for logging. I've already read those, and it's left me still not quite certain how it's better than peppering my code with printfs. It must be, because it's there, and people use it. But it's still not clear to me how to use it to save time rather than to learn and type a whole new bunch of stuff. \$\endgroup\$ – Neil Thomas Jun 25 '12 at 21:51
  • 2
    \$\begingroup\$ logging is a lot more flexible in terms of filtering. You can add debugging messages all over the place, then turn them all on or off at once by setting the logging level, while leaving warnings and errors active. Other people using your code can arrange for your logging statements to route to the console, to log files, or to email a sysadmin, or whatever else they want, without changing the internals of your code, etc. Mostly it's somewhat impolite to spew directly to the console from a library that you intend other people to use. \$\endgroup\$ – Russell Borogove Jun 25 '12 at 22:32
  • 1
    \$\begingroup\$ I'm going to concur with Russell Borogove. One python paradigm is "there should be only one obvious way of doing things." In this case logging would be it. Note that the first section of the Logging HOWTO provides a handy table of when to use and when not to use logging. \$\endgroup\$ – Joel Cornett Jun 30 '12 at 0:25
0
\$\begingroup\$

I think I've done enough thinking in the last week to answer my own question with an update. Thanks very much for all the comments here, and to a related OT post on stackoverflow.

Some of my errors were not knowing about the batteries so I was reinventing wheels. I've kept the things from my first attempt that were specific objectives. However, the comments have helped me to acheive it in a much better way. To make my example clearer, I've stripped out all functionality, leaving only a pattern for the bare class.

Just as python's strict use of indentation can be justified as "you've got to get it right or it doesn't work", the style of this class is "you have to enter the property names, defaults and types, or it doesn't work". But once you have, the class sets itself up, is self checking and self documenting, up to a point. Moving those to class attributes is an obvious improvement.

Maybe the config() paradigm is frowned upon, but the first non-trivial programming I did in python was using tKinter, which uses that throughout consistently, and I've quite grown to like it. I like the flexibility to be able to instantiate, configure, trigger an object, with freedom for when key properties are set, especially when many are defaulted, or worse not defaulted but don't need to change from the last config. In additon, changing a property should often trigger some action in the objects I envisage, so doing it via a method is the right thing to do.

Spill() was only really for rapid and lazy introspection. Now I've found out about pprint and obj.__dict__, that removes my need for it, and it doesn't really need aliasing. Calling my argument list .usage makes that apparent as well.

Perhaps raising errors for things that could be warnings is a bit draconian, but hey, you've got to get it right or it won't work. I'm not sure I see the need to subclass those yet, maybe later. I'll find out how to use logging in due course. I've a list as long as my arm for things to master yet (pyaudio, {} formatting, matplotlib, etc etc) before that.

I did start to put value checking into the config() function, before realising that there was little point in config bleating about max and min limits being exceeded, when application specific functions would also have to check more complicated value related stuff, like relationships between values. Basically, I felt that value checking in config would be unnecessary and insufficient. That, and reading the meaning of YAGNI on c2.com. Config will simply end with a call to self.consistency_check(), which is use-specific.

Anyhow, this is what I've ended up with. Any more comments on the style, pythonicity etc for points I've not already covered above would be greatly appreciated.

class AnyClass():
    """ A pattern for a configurable class
    sort of EnthoughtTraitsLite
    """

    # argument descriptor tuples (namestr,default,types_tuple)
    usage=[('period',1,('float','int','long')),
           ('func_tick','func_tick not defined',('function','str')),
           ('max_ticks',0,('int','long')),
           ('func_done','func_done not defined',('function','str')),
           ('max_overruns',0,('int','long')),
           ('func_over','func_over not defined',('function','str')),
           ('test_anytype',0,'')]

    # property descriptor tuples (namestr,default,types_tuple)
    private=[('_N_ticks',0,'int'),
             ('_N_overruns',0,'int'),
             ('_t_next_tick',0,'float')]

    def __init__(self,**kwargs):        
        # set up properties
        for prop in self.usage+self.private:
            setattr(self,prop[0],prop[1])
        # eat whatever has been passed to it at setup
        self.config(**kwargs)

    def config(self,**kwargs):
        for prop_desc in self.usage:
            argname=prop_desc[0]
            OKtypes=prop_desc[2]
            if argname in kwargs:
                argval=kwargs.pop(argname)
                argtype=type(argval).__name__
                if OKtypes and (argtype not in OKtypes):
                    raise TypeError('assignment to '+argname+' is type '+argtype+', should be in '+str(OKtypes))
                else:
                    setattr(self,argname,argval)

        if kwargs:
            raise NameError('unknown parameter(s) '+repr(kwargs)+' supplied')       


if __name__=='__main__':    
    import pprint
    dump=pprint.pprint

    def hello():
        print 'hello world'

    q=AnyClass()
    print 'usage'
    dump(q.usage)

    try:
        # q.config(max_ticks=3.5)
        q.config(max_ticks=-4)
        # q.config(interloper=3)    
        q.config(func_tick=hello,max_ticks=3)
        q.config(test_anytype=3)
    except Exception as error:
        print
        dump(error)

    print
    print 'show all properties'
    dump(q.__dict__)

    a=raw_input('press return to quit - ')
| improve this answer | |
\$\endgroup\$

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.