Test Your Knowledge: Answers

  1. 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
    
  2. 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
    
  3. 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'>