Ah what the heck, here goes my explanation:
Babushka (grandma) matryoshka dolls |
See the following function declarations:
def wrap(f) # Add before behaviour print "start wrap" # Do something with f # Add after behaviour print "end wrap" return f # Pass f to the next label or babushka/function @wrap def fun(): pass # do something interesting...
The previous rows are equivalent to
fun = wrap(fun)
The wrapping function (wrap) must return a final function to relabel the newly decorated function with the original unwrapped label, which is fun in our example.
You could also decorate more than once:
@huge @medium @small def core() pass
Which is equivalent to the following:
core = huge(medium(small(core)))In this example, one must also keep passing the decorated function to the next decorator with a return statement.
This year I decided to improve my mental arithmetic skills, so I wrote a python application to train on. I was also quite curious as to what the whole python decorator thing was about. I combined my curiosity for python decorators and the desire for a mental arithmetic trainer. The following script is the (partial) end result.
def timeit(fun): from time import time def timed(*args): ts = time() mistakes, op_symbol = fun(*args) te = time() time_elapsed = te - ts print "Time elapsed:", time_elapsed return time_elapsed, mistakes, op_symbol return timed class Query(object): def __init__(self, op_symbol): self.op_symbol = op_symbol def __call__(self, f): # NOTE: you can count the function arguments later to try on a n-ary functions def wrapped_binary_f(*args): a,b = tuple(args) #print "%s %s %s = ?" % (a, self.op_symbol, b) # infix notation print a, self.op_symbol, b, " = ?" correct_answer = f(a,b) user_answer = None mistakes = 0 while not user_answer or user_answer != correct_answer: user_answer = None try: user_answer = input("answer: ") if user_answer != correct_answer: mistakes+=1 print "Please try again... %s" % str(mistakes) else: print "Correct." break except SyntaxError: mistakes+=1 print "Please try again... %s" % str(mistakes) except NameError: mistakes+=1 print "Please try again... %s" % str(mistakes) return mistakes, self.op_symbol return wrapped_binary_f @timeit @Query('*') def multiply(a, b): return a * b @timeit @Query('-') def subtract(a, b): return a - b @timeit @Query('+') def add(a, b): return a + b
This example demonstrates three techniques, namely:
- Passing arguments to a decorator function (Query class's __init__ function);
- Using multiple decorator functions on multiple functions (timeit, Query);
- Manipulating arguments passed to the decorator function and the called functions (multiply, subtract, add) and mixing them up
One can also add arguments to the decorator function by way of a class declaration. First the __init__ function is called to create the object of the class arguments passed, the arguments are stored in the object, then the object is called via the __call__ function. The __call__ function can access the arguments from the members of the object.
I have included the complete application here, try it out, improve it, enjoy!
#!/usr/bin/env python # -*- coding: iso-8859-1 -*- ''' Command-line interactive script to train mental arithmetic, also a demonstration of python decorators. Created on May 22, 2010 and refactored several times after... ''' def timeit(fun): from time import time def timed(*args): ts = time() mistakes, op_symbol = fun(*args) te = time() time_elapsed = te - ts print "Time elapsed:", time_elapsed return time_elapsed, mistakes, op_symbol return timed class Query(object): def __init__(self, op_symbol): self.op_symbol = op_symbol def __call__(self, f): # NOTE: you can count the function arguments later to try on a n-ary function def wrapped_binary_f(*args): a,b = tuple(args) #print "%s %s %s = ?" % (a, self.op_symbol, b) # infix notation print a, self.op_symbol, b, " = ?" correct_answer = f(a,b) user_answer = None mistakes = 0 while not user_answer or user_answer != correct_answer: user_answer = None try: user_answer = input("answer: ") if user_answer != correct_answer: mistakes+=1 print "Please try again... %s" % str(mistakes) else: print "Correct." break except SyntaxError: mistakes+=1 print "Please try again... %s" % str(mistakes) except NameError: mistakes+=1 print "Please try again... %s" % str(mistakes) return mistakes, self.op_symbol return wrapped_binary_f @timeit @Query('*') def multiply(a, b): return a * b @timeit @Query('-') def subtract(a, b): return a - b @timeit @Query('+') def add(a, b): return a + b @timeit @Query('/') def divide(a, b): return float(a) / float(b) # from decimal import Decimal, getcontext # from fractions import Fraction # check this shit out, useful if user enters fraction and computer calculates float or Decimal... then check if the same # getcontext().prec=2 # return Decimal(a) / Decimal(b) def generate_samples_naive(maxdecimals, nsamples): from random import choice return zip([ choice(xrange(10**maxdecimals)) for _i in xrange(nsamples) ], [ choice(xrange(10**maxdecimals)) for _j in xrange(nsamples) ]) def generate_samples_positive(maxdecimals, nsamples): """ The lambda here swaps the values in case subtraction results are negative """ from random import choice return map (lambda (x,y): (x,y) if x>=y else (y,x), \ [ (choice(xrange(10**maxdecimals)), choice(xrange(10**maxdecimals))) \ for _i in xrange(nsamples) ]) def get_stats(li): """ Tuple of total time spent solving the problems and total number of mistakes """ return sum([ t for t,_,_ in li ]), \ sum([m for _,m,_ in li]) def get_detailed_stats(li): """ Total number of mistakes per operator type, total number of time spent per operator type """ d = {} for op_symbol in ['*','-','+','/']: d[op_symbol] = (sum([ t for t,_,o in li if o == op_symbol ]), sum([ m for _,m,o in li if o == op_symbol ])) return d def show_stats(results): (total_time, total_mistakes) = get_stats(results) print "Total solved:", len(results) print "Total time elapsed:", total_time print "Total mistakes made:", total_mistakes print "Detailed results per operator (time, mistakes):" #, get_detailed_stats(results) d = get_detailed_stats(results) for k in d.keys(): (time, mistakes) = d[k] print k, "%.2f" % (float(time)/total_time), "%.2f" % (float(mistakes)/total_mistakes) print "Done!" def main(av): from optparse import OptionParser usage = "usage: %prog [options] arg" parser = OptionParser(usage) # parser.add_option... # parser.add_option("-m", "--max-tries", dest='maxtries', type='int', default=3, # help='Number of tries before going to the next example') parser.add_option("-n", "--nr-samples", dest="nsamples", type="int", default=10, help="Per arithmetic operation train on this number of SAMPLES", metavar="SAMPLES") # parser.add_option("--mind", "--min-decimals", dest="maxdecimals", type="int", default=0, # help="Define the lowest number to train expressed as the number of DECIMALS", metavar="DECIMALS") parser.add_option("--maxd", "--max-decimals", dest="maxdecimals", type="int", default=2, help="Define the largest number to train expressed as the number of DECIMALS", metavar="DECIMALS") # parser.add_option("-s", "--no-stats", dest="sendstats", action="store_false", default=True, # help="Don't send any statistical data to servers, over the internet.") # parser.add_option("-a", "--anonymous", dest="anonymous", action="store_true", default=False, # help="Don't send credentials or any data revealing user's identity over the internet") parser.add_option("--nd", "--no-division", dest="train_division", action="store_false", default=False, help="Don't train user on division") parser.add_option("--na", "--no-addition", dest="train_addition", action="store_false", default=True, help="Don't train user on division") parser.add_option("--ns", "--no-subtraction", dest="train_subtraction", action="store_false", default=True, help="Don't train user on subtraction") parser.add_option("--nm", "--no-multiplication", dest="train_multiplication", action="store_false", default=True, help="Don't train user on multiplication") parser.add_option("--nh", "--no-history", dest="store_history", action="store_false", default=True, help="Don't store user history, mind you, the same training samples might be repeated very often") parser.add_option("-o", "--ordered-training", dest="ordered", action="store_false", default=True, help="Train in this order: addition, subtraction, multiplication, division") parser.add_option("-e", "--e-mail-addr", dest="email_addr", type="string", default="anon@anon.org", help="This script will modify itself to reflect the e-mail ADDRESS of the user in question", metavar="ADDRESS") parser.add_option("-p","--positive-only", dest="positive_only", action="store_true", default=False, help="The result from the operation on a pair of samples will only be POSITIVE.", metavar="POSITIVE") (options, _args) = parser.parse_args(av) if options.positive_only: samples = generate_samples_positive(options.maxdecimals, options.nsamples) else: samples = generate_samples_naive(options.maxdecimals, options.nsamples) results = [] if options.ordered: if options.train_subtraction: for (a,b) in samples: results.append(subtract(a,b)) if options.train_addition: for (a,b) in samples: results.append(add(a,b)) if options.train_multiplication: for (a,b) in samples: results.append(multiply(a,b)) if options.train_division: for (a,b) in samples: results.append(divide(a,b)) else: op_options = [] if options.train_addition: op_options.append(add) if options.train_multiplication: op_options.append(multiply) if options.train_subtraction: op_options.append(subtract) if options.train_divison: op_options.append(divide) fns = op_options*options.nsamples import random random.shuffle(fns) for (nr,(a,b)) in enumerate(3*samples): # TODO - come division, change to four results.append(fns[nr](a,b)) show_stats(results) if __name__ == '__main__': from sys import argv as av main(av)
Update 30-07-2010: I just realized that you could grab this code and put a gui on top quite easily, by replacing the Query class with something with a gui. Done!
No comments:
Post a Comment
Please help to keep this blog clean. Don't litter with spam.