Here’s one way to code the first question’s solution, and its output (though some methods may run too fast to register reported time). The trick lies in replacing nested classes with nested functions, so the self argument is not the decorator’s instance, and assigning the total time to the decorator function itself so it can be fetched later through the original rebound name (see the section “State Information Retention Options” of this chapter for details—functions support arbitrary attribute attachment, and the function name is an enclosing scope reference in this context). If you wish to expand this further, it might be useful to also record the best (minimum) call time in addition to the total time, as we did in Chapter 21’s timer examples.
""" File timerdeco.py (3.X + 2.X) Call timer decorator for both functions and methods. """ import time def timer(label='', trace=True): # On decorator args: retain args def onDecorator(func): # On @: retain decorated func def onCall(*args, **kargs): # On calls: call original start = time.clock() # State is scopes + func attr result = func(*args, **kargs) elapsed = time.clock() - start onCall.alltime += elapsed if trace: format = '%s%s: %.5f, %.5f' values = (label, func.__name__, elapsed, onCall.alltime) print(format % values) return result onCall.alltime = 0 return onCall return onDecorator
I’ve coded tests in a separate file here to allow the decorator to be easily reused:
"""
File timerdeco-test.py
"""
from __future__ import print_function # 2.X
from timerdeco import timer
import sys
force = list if sys.version_info[0] == 3 else (lambda X: X)
print('---------------------------------------------------')
# Test on functions
@timer(trace=True, label='[CCC]==>')
def listcomp(N): # Like listcomp = timer(...)(listcomp)
return [x * 2 for x in range(N)] # listcomp(...) triggers onCall
@timer('[MMM]==>')
def mapcall(N):
return force(map((lambda x: x * 2), range(N))) # list() for 3.X views
for func in (listcomp, mapcall):
result = func(5) # Time for this call, all calls, return value
func(5000000)
print(result)
print('allTime = %s\n' % func.alltime) # Total time for all calls
print('---------------------------------------------------')
# Test on methods
class Person:
def __init__(self, name, pay):
self.name = name
self.pay = pay
@timer()
def giveRaise(self, percent): # giveRaise = timer()(giveRaise)
self.pay *= (1.0 + percent) # tracer remembers giveRaise
@timer(label='**')
def lastName(self): # lastName = timer(...)(lastName)
return self.name.split()[-1] # alltime per class, not instance
bob = Person('Bob Smith', 50000)
sue = Person('Sue Jones', 100000)
bob.giveRaise(.10)
sue.giveRaise(.20) # runs onCall(sue, .10)
print(int(bob.pay), int(sue.pay))
print(bob.lastName(), sue.lastName()) # runs onCall(bob), remembers lastName
print('%.5f %.5f' % (Person.giveRaise.alltime, Person.lastName.alltime))
If all goes according to plan, you’ll see the following output in both Python 3.X and 2.X, albeit with timing results that will vary per Python and machine:
c:\code> py −3 timerdeco-test.py
---------------------------------------------------
[CCC]==>listcomp: 0.00001, 0.00001
[CCC]==>listcomp: 0.57930, 0.57930
[0, 2, 4, 6, 8]
allTime = 0.5793010457092784
[MMM]==>mapcall: 0.00002, 0.00002
[MMM]==>mapcall: 1.08609, 1.08611
[0, 2, 4, 6, 8]
allTime = 1.0861149923442373
---------------------------------------------------
giveRaise: 0.00001, 0.00001
giveRaise: 0.00000, 0.00001
55000 120000
**lastName: 0.00001, 0.00001
**lastName: 0.00000, 0.00001
Smith Jones
0.00001 0.00001
The following three files satisfy the second question. The first gives the decorator—it’s been augmented to return the original class in optimized mode (–O), so attribute accesses don’t incur a speed hit. Mostly, it just adds the debug mode test statements and indents the class further to the right:
"""
File access.py (3.X + 2.X)
Class decorator with Private and Public attribute declarations.
Controls external access to attributes stored on an instance, or
inherited by it from its classes in any fashion.
Private declares attribute names that cannot be fetched or assigned
outside the decorated class, and Public declares all the names that can.
Caveats: in 3.X catches built-ins coded in BuiltinMixins only (expand me);
as coded, Public may be less useful than Private for operator overloading.
"""
from access_builtins import BuiltinsMixin # A partial set!
traceMe = False
def trace(*args):
if traceMe: print('[' + ' '.join(map(str, args)) + ']')
def accessControl(failIf):
def onDecorator(aClass):
if not __debug__:
return aClass
else:
class onInstance(BuiltinsMixin):
def __init__(self, *args, **kargs):
self.__wrapped = aClass(*args, **kargs)
def __getattr__(self, attr):
trace('get:', attr)
if failIf(attr):
raise TypeError('private attribute fetch: ' + attr)
else:
return getattr(self.__wrapped, attr)
def __setattr__(self, attr, value):
trace('set:', attr, value)
if attr == '_onInstance__wrapped':
self.__dict__[attr] = value
elif failIf(attr):
raise TypeError('private attribute change: ' + attr)
else:
setattr(self.__wrapped, attr, value)
return onInstance
return onDecorator
def Private(*attributes):
return accessControl(failIf=(lambda attr: attr in attributes))
def Public(*attributes):
return accessControl(failIf=(lambda attr: attr not in attributes))
I’ve also used one of our mix-in techniques to add some operator overloading method redefinitions to the wrapper class, so that in 3.X it correctly delegates built-in operations to subject classes that use these methods. As coded, the proxy is a default classic class in 2.X that routes these through __getattr__ already, but in 3.X is a new-style class that does not. The mix-in used here requires listing such methods in Public decorators; see earlier for alternatives that do not (but that also do not allow built-ins to be made private), and expand this class as needed:
"""
File access_builtins.py (from access2_builtins2b.py)
Route some built-in operations back to proxy class __getattr__, so they
work the same in 3.X as direct by-name calls and 2.X's default classic classes.
Expand me as needed to include other __X__ names used by proxied objects.
"""
class BuiltinsMixin:
def reroute(self, attr, *args, **kargs):
return self.__class__.__getattr__(self, attr)(*args, **kargs)
def __add__(self, other):
return self.reroute('__add__', other)
def __str__(self):
return self.reroute('__str__')
def __getitem__(self, index):
return self.reroute('__getitem__', index)
def __call__(self, *args, **kargs):
return self.reroute('__call__', *args, **kargs)
# Plus any others used by wrapped objects in 3.X only
Here too I split the self-test code off to a separate file, so the decorator could be imported elsewhere without triggering the tests, and without requiring a __name__ test and indenting:
"""
File: access-test.py
Test code: separate file to allow decorator reuse.
"""
import sys
from access import Private, Public
print('---------------------------------------------------------')
# Test 1: names are public if not private
@Private('age') # Person = Private('age')(Person)
class Person: # Person = onInstance with state
def __init__(self, name, age):
self.name = name
self.age = age # Inside accesses run normally
def __add__(self, N):
self.age += N # Built-ins caught by mix-in in 3.X
def __str__(self):
return '%s: %s' % (self.name, self.age)
X = Person('Bob', 40)
print(X.name) # Outside accesses validated
X.name = 'Sue'
print(X.name)
X + 10
print(X)
try: t = X.age # FAILS unless "python -O"
except: print(sys.exc_info()[1])
try: X.age = 999 # ditto
except: print(sys.exc_info()[1])
print('---------------------------------------------------------')
# Test 2: names are private if not public
# Operators must be non-Private or Public in BuiltinMixin used
@Public('name', '__add__', '__str__', '__coerce__')
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def __add__(self, N):
self.age += N # Built-ins caught by mix-in in 3.X
def __str__(self):
return '%s: %s' % (self.name, self.age)
X = Person('bob', 40) # X is an onInstance
print(X.name) # onInstance embeds Person
X.name = 'sue'
print(X.name)
X + 10
print(X)
try: t = X.age # FAILS unless "python -O"
except: print(sys.exc_info()[1])
try: X.age = 999 # ditto
except: print(sys.exc_info()[1])
Finally, if all works as expected, this test’s output is as follows in both Python 3.X and 2.X—the same code applied to the same class decorated with Private and then with Public:
c:\code>py −3 access-test.py--------------------------------------------------------- Bob Sue Sue: 50 private attribute fetch: age private attribute change: age --------------------------------------------------------- bob sue sue: 50 private attribute fetch: age private attribute change: age c:\code>py −3 -O access-test.py# Suppresses the four access error messages
Here’s a generalized argument validator for you to study on your own. It uses a passed-in validation function, to which it passes the test’s criteria value coded for the argument in the decorator. This handles ranges, type tests, value testers, and almost anything else you can dream up in an expressive language like Python. I’ve also refactored the code a bit to remove some redundancy, and automated test failure processing. See this module’s self-test for usage examples and expected output. Per this example’s caveats described earlier, this decorator doesn’t fully work in nested mode as is—only the most deeply nested validation is run for positional arguments—but its arbitrary valuetest can be used to combine differing types of tests in a single decoration (though the amount of code needed in this mode may negate much of its benefits over a simple assert!).
"""
File argtest.py: (3.X + 2.X) function decorator that performs
arbitrary passed-in validations for arguments passed to any
function method. Range and type tests are two example uses;
valuetest handles more arbitrary tests on an argument's value.
Arguments are specified by keyword to the decorator. In the actual
call, arguments may be passed by position or keyword, and defaults
may be omitted. See self-test code below for example use cases.
Caveats: doesn't fully support nesting because call proxy args
differ; doesn't validate extra args passed to a decoratee's *args;
and may be no easier than an assert except for canned use cases.
"""
trace = False
def rangetest(**argchecks):
return argtest(argchecks, lambda arg, vals: arg < vals[0] or arg > vals[1])
def typetest(**argchecks):
return argtest(argchecks, lambda arg, type: not isinstance(arg, type))
def valuetest(**argchecks):
return argtest(argchecks, lambda arg, tester: not tester(arg))
def argtest(argchecks, failif): # Validate args per failif + criteria
def onDecorator(func): # onCall retains func, argchecks, failif
if not __debug__: # No-op if "python -O main.py args..."
return func
else:
code = func.__code__
expected = list(code.co_varnames[:code.co_argcount])
def onError(argname, criteria):
errfmt = '%s argument "%s" not %s'
raise TypeError(errfmt % (func.__name__, argname, criteria))
def onCall(*pargs, **kargs):
positionals = expected[:len(pargs)]
for (argname, criteria) in argchecks.items(): # For all to test
if argname in kargs: # Passed by name
if failif(kargs[argname], criteria):
onError(argname, criteria)
elif argname in positionals: # Passed by posit
position = positionals.index(argname)
if failif(pargs[position], criteria):
onError(argname, criteria)
else: # Not passed-dflt
if trace:
print('Argument "%s" defaulted' % argname)
return func(*pargs, **kargs) # OK: run original call
return onCall
return onDecorator
if __name__ == '__main__':
import sys
def fails(test):
try: result = test()
except: print('[%s]' % sys.exc_info()[1])
else: print('?%s?' % result)
print('--------------------------------------------------------------------')
# Canned use cases: ranges, types
@rangetest(m=(1, 12), d=(1, 31), y=(1900, 2013))
def date(m, d, y):
print('date = %s/%s/%s' % (m, d, y))
date(1, 2, 1960)
fails(lambda: date(1, 2, 3))
@typetest(a=int, c=float)
def sum(a, b, c, d):
print(a + b + c + d)
sum(1, 2, 3.0, 4)
sum(1, d=4, b=2, c=3.0)
fails(lambda: sum('spam', 2, 99, 4))
fails(lambda: sum(1, d=4, b=2, c=99))
print('--------------------------------------------------------------------')
# Arbitrary/mixed tests
@valuetest(word1=str.islower, word2=(lambda x: x[0].isupper()))
def msg(word1='mighty', word2='Larch', label='The'):
print('%s %s %s' % (label, word1, word2))
msg() # word1 and word2 defaulted
msg('majestic', 'Moose')
fails(lambda: msg('Giant', 'Redwood'))
fails(lambda: msg('great', word2='elm'))
print('--------------------------------------------------------------------')
# Manual type and range tests
@valuetest(A=lambda x: isinstance(x, int), B=lambda x: x > 0 and x < 10)
def manual(A, B):
print(A + B)
manual(100, 2)
fails(lambda: manual(1.99, 2))
fails(lambda: manual(100, 20))
print('--------------------------------------------------------------------')
# Nesting: runs both, by nesting proxies on original.
# Open issue: outer levels do not validate positionals due
# to call proxy function's differing argument signature;
# when trace=True, in all but the last of these "X" is
# classified as defaulted due to the proxy's signature.
@rangetest(X=(1, 10))
@typetest(Z=str) # Only innermost validates positional args
def nester(X, Y, Z):
return('%s-%s-%s' % (X, Y, Z))
print(nester(1, 2, 'spam')) # Original function runs properly
fails(lambda: nester(1, 2, 3)) # Nested typetest is run: positional
fails(lambda: nester(1, 2, Z=3)) # Nested typetest is run: keyword
fails(lambda: nester(0, 2, 'spam')) # <==Outer rangetest not run: posit.
fails(lambda: nester(X=0, Y=2, Z='spam')) # Outer rangetest is run: keyword
This module’s self-test output in both 3.X and 2.X follows (some 2.X object displays vary slightly): as usual, correlate with the source for more insights.
c:\code> py −3 argtest.py
--------------------------------------------------------------------
date = 1/2/1960
[date argument "y" not (1900, 2013)]
10.0
10.0
[sum argument "a" not <class 'int'>]
[sum argument "c" not <class 'float'>]
--------------------------------------------------------------------
The mighty Larch
The majestic Moose
[msg argument "word1" not <method 'islower' of 'str' objects>]
[msg argument "word2" not <function <lambda> at 0x0000000002A096A8>]
--------------------------------------------------------------------
102
[manual argument "A" not <function <lambda> at 0x0000000002A09950>]
[manual argument "B" not <function <lambda> at 0x0000000002A09B70>]
--------------------------------------------------------------------
1-2-spam
[nester argument "Z" not <class 'str'>]
[nester argument "Z" not <class 'str'>]
?0-2-spam?
[onCall argument "X" not (1, 10)]
Finally, as we’ve learned, this decorator’s coding structure works for both functions and methods:
# File argtest_testmeth.py from argtest import rangetest, typetest class C: @rangetest(a=(1, 10)) def meth1(self, a): return a * 1000 @typetest(a=int) def meth2(self, a): return a * 1000 >>>from argtest_testmeth import C>>>X = C()>>>X.meth1(5)5000 >>>X.meth1(20)TypeError: meth1 argument "a" not (1, 10) >>>X.meth2(20)20000 >>>X.meth2(20.9)TypeError: meth2 argument "a" not <class 'int'>