Online Book Reader

Home Category

Learning Python - Mark Lutz [554]

By Root 1383 0
Here, we mean to validate the values of function arguments when passed, rather than attribute values when set. Python’s decorator and introspection tools allow us to code this new task just as easily.

A Basic Range-Testing Decorator for Positional Arguments

Let’s start with a basic range test implementation. To keep things simple, we’ll begin by coding a decorator that works only for positional arguments and assumes they always appear at the same position in every call; they cannot be passed by keyword name, and we don’t support additional **args keywords in calls because this can invalidate the positions declared in the decorator. Code the following in a file called devtools.py:

def rangetest(*argchecks): # Validate positional arg ranges

def onDecorator(func):

if not __debug__: # True if "python -O main.py args..."

return func # No-op: call original directly

else: # Else wrapper while debugging

def onCall(*args):

for (ix, low, high) in argchecks:

if args[ix] < low or args[ix] > high:

errmsg = 'Argument %s not in %s..%s' % (ix, low, high)

raise TypeError(errmsg)

return func(*args)

return onCall

return onDecorator

As is, this code is mostly a rehash of the coding patterns we explored earlier: we use decorator arguments, nested scopes for state retention, and so on.

We also use nested def statements to ensure that this works for both simple functions and methods, as we learned earlier. When used for a class method, onCall receives the subject class’s instance in the first item in *args and passes this along to self in the original method function; argument numbers in range tests start at 1 in this case, not 0.

Also notice this code’s use of the __debug__ built-in variable, though—Python sets this to True, unless it’s being run with the –O optimize command-line flag (e.g., python –O main.py). When __debug__ is False, the decorator returns the origin function unchanged, to avoid extra calls and their associated performance penalty.

This first iteration solution is used as follows:

# File devtools_test.py

from devtools import rangetest

print(__debug__) # False if "python –O main.py"

@rangetest((1, 0, 120)) # persinfo = rangetest(...)(persinfo)

def persinfo(name, age): # age must be in 0..120

print('%s is %s years old' % (name, age))

@rangetest([0, 1, 12], [1, 1, 31], [2, 0, 2009])

def birthday(M, D, Y):

print('birthday = {0}/{1}/{2}'.format(M, D, Y))

class Person:

def __init__(self, name, job, pay):

self.job = job

self.pay = pay

@rangetest([1, 0.0, 1.0]) # giveRaise = rangetest(...)(giveRaise)

def giveRaise(self, percent): # Arg 0 is the self instance here

self.pay = int(self.pay * (1 + percent))

# Comment lines raise TypeError unless "python -O" used on shell command line

persinfo('Bob Smith', 45) # Really runs onCall(...) with state

#persinfo('Bob Smith', 200) # Or person if –O cmd line argument

birthday(5, 31, 1963)

#birthday(5, 32, 1963)

sue = Person('Sue Jones', 'dev', 100000)

sue.giveRaise(.10) # Really runs onCall(self, .10)

print(sue.pay) # Or giveRaise(self, .10) if –O

#sue.giveRaise(1.10)

#print(sue.pay)

When run, valid calls in this code produce the following output (all the code in this section works the same under Python 2.6 and 3.0, because function decorators are supported in both, we’re not using attribute delegation, and we use 3.0-style print calls and exception construction syntax):

C:\misc> C:\python30\python devtools_test.py

True

Bob Smith is 45 years old

birthday = 5/31/1963

110000

Uncommenting any of the invalid calls causes a TypeError to be raised by the decorator. Here’s the result when the last two lines are allowed to run (as usual, I’ve omitted some of the error message text here to save space):

C:\misc> C:\python30\python devtools_test.py

True

Bob Smith is 45 years old

birthday = 5/31/1963

110000

TypeError: Argument 1 not in 0.0..1.0

Running Python with its –O flag at a system command line will disable range testing, but also avoid the performance overhead of the wrapping layer—we wind up calling the original

Return Main Page Previous Page Next Page

®Online Book Reader