python 修饰器用法2

I presented a couple of examples on how to use function decorators in Python. Those examples were illustrative, yet fairly restricted. First of all, they assumed that the function being decorated only receives one positional argument. What can I do if I want to decorate a function that takes two or more positional arguments, or one or more keyword arguments? Secondly, they didn't allow the decorator to be configured in any special way. So, how do I send input arguments to the decorator itself so that I can vary its behavior?

I will now elaborate in a slightly more complex example that will give us a better insight on how to define much more general decorators, without any of the restrictions I just mentioned.

NOTE: All Python code examples presented here are based in Python 3.0. Full source code: more_function_decorators.py

Suppose we want a decorator function that is able to "swallow" one or more kinds of exceptions that might be raised during the execution of the decorated function1. If a particular exception is actually produced, we want to be able to specify a default value to be returned instead of allowing the exception to propagate through the execution stack and possibly causing the program to terminate. A client of our decorator would basically be able to avoid the hassle of writing an explicit try statement.

To demonstrate how this could work, let's assume we have a function like this one:

1
2
def divide(dividend=0, divisor=1):
    return dividend / divisor

When calling this function, there are at least two possible exceptions that might get raised: ZeroDivisionError and TypeError. The first one is produced when the divisor parameter is zero. The second exception occurs when any of the two arguments sent are not of a numerical type, when more than two arguments are actually sent, or when you try to use a nonexistent keyword argument. Some examples:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
>>> divide(1, 2)
0.5
 
>>> divide(1, 0)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in divide
ZeroDivisionError: int division or modulo by zero
 
>>> divide("hello")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in divide
TypeError: unsupported operand type(s) for /: 'str' 
and 'int' 
 
>>> divide(whatever=1)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: divide() got an unexpected keyword 
argument 'whatever'

Our swallow decorator will receive two keyword arguments:

  • exceptions: a single exception class or a tuple containing several exception classes. These represent the exceptions that should be swallowed. Any other exception will be propagated as usual. Defaults to BaseException (the root of Python's exception hierarchy) if it's not explicitly provided.
  • default: the value to return if one of the specified exceptions is raised. Defaults to None when not provided.

Let's look at three usage examples.

Example 1: If a division by zero is attempted, return zero:

1
2
3
@swallow(exceptions=ZeroDivisionError, default=0)
def divide(dividend=0, divisor=1):
    return dividend / divisor

Example 2: If a ZeroDivisionError or a TypeError is raised, return zero:

1
2
3
4
5
@swallow(
    exceptions=(ZeroDivisionError, TypeError), 
    default=0)
def divide(dividend=0, divisor=1):
    return dividend / divisor

Example 3: Chain two swallow decorators, so that each specific exception has its own default value:

1
2
3
4
@swallow(exceptions=TypeError, default='Huh?')
@swallow(exceptions=ZeroDivisionError, default=0)
def divide(dividend=0, divisor=1):
    return dividend / divisor

For this last example, this is how we could now use our decorated divide function:

1
2
3
4
5
6
>>> divide(1, 2)
0.5
>>> divide(1, 0)
0
>>> divide("hello")
'Huh?'

In order to implement the swallow decorator, we must take a better look on how the @ syntax works. After the @ sign, you must actually specify an expression that when evaluated produces a callable object2. When this callable object is effectively called, it receives as its only argument the function that is to be decorated, and it returns that same function or some new callable thing.

The expression after the @ sign is commonly just the name of a function (the decorator function), but it can also be: 1) a new instance of a class that contains an implementation of the __call__ method; or 2) a call to some other function. Both these options allow us to send additional information to the decorator by specifying it via input parameters.

A first implementation using classes that define the __call__ method could be as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class swallow:
    
    class helper:
        
        def __init__(self, outer, fun):
            self.outer = outer
            self.fun = fun            
            
        def __call__(self, *args, **kwargs):
            try:
                return self.fun(*args, **kwargs)
            except self.outer.exceptions:
                return self.outer.default
    
    def __init__(self, 
                 default=None, 
                 exceptions=BaseException):
        self.default = default
        self.exceptions = exceptions
        
    def __call__(self, fun):                            
        return swallow.helper(self, fun)

Let's try to understand how this works. This code:

1
2
3
@swallow(exceptions=ZeroDivisionError, default=0)
def divide(dividend=0, divisor=1):
    return dividend / divisor

is basically equivalent to this one:

1
2
3
4
def divide(dividend=0, divisor=1):
    return dividend / divisor     
divide = swallow(exceptions=ZeroDivisionError, 
                 default=0).__call__(divide)

In the last statement of the above code, an instance of the swallow class is created and initialized according to our needs. Then, the__call__ method is invoked on that very same instance, which in turn creates and returns a new instance of the swallow.helpernested class. The final effect is that the divide variable refers to an instance of this specific class. 

The swallow.helper class implements a __call__ method that accepts any number of positional and keyword arguments3. Thus, instances of this class can effectively decorate any function that takes whatever arguments it needs. The __call__ method itself contains a try statement that does all the work: it invokes the decorated function and sends back the returned value, unless any of the specified exceptions get caught, in which case it returns the specified default value. Note that all the values that are required to do the job are conveniently stored and shared using the instance variables of the swallow and swallow.helper classes.

A second implementation of the swallow decorator can be coded using only function definitions (and their corresponding lexical closures). Although it's considerably shorter, it might be a little bit more difficult to understand at first:

1
2
3
4
5
6
7
8
9
def swallow(default=None, exceptions=BaseException):
    def helper1(fun):
        def helper2(*args, **kwargs):
            try:
                return fun(*args, **kwargs)
            except exceptions:
                return default
        return helper2
    return helper1

As can be observed, the swallow function takes the two input parameters that allow us to configure the decorator. It returns the nested function helper1, which is just what the @ syntax expects. The helper1 function will be immediately called with the function being decorated as its only argument, returning function helper2 as its result. This means that if we are decorating a function called f, variable f will end up holding a reference to the helper2 function. So now, whenever f gets invoked, helper2 will be called and the try statement will do its job exactly as described before.

Notes

1 Swallowing exceptions can be convenient under certain circumstances, but you should avoid using this technique indiscriminately. Specifically, it can make debugging code very hard.

2 Callable objects contain a special attribute named __call__. If x is a callable object, then the syntax:

1
x(arg1, arg2, arg3)

is equivalent to:

1
x.__call__(arg1, arg2, arg3)

Callable objects include user-defined functions, built-in functions, methods of built-in objects, class objects, methods of class instances, and instances of classes that define or inherit their own __call__ method.

3 If your not familiar with the *args and **kwargs notation, check the Python tutorial for more details.



  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值