装饰器跳过`self`方法

时间:2018-09-18 17:23:14

标签: python

比方说,我们有多个函数都接受一个URL作为其第一个参数,并且该URL需要进行验证。这可以用装饰器很好地解决

def validate_url(f):
   def validated(url, *args, **kwargs):
       assert len(url.split('.')) == 3 # trivial example
       return f(url, *args, **kwargs)
    return validated

@validate_url
def some_func(url, some_other_arg, *some_args, **some_kwargs):
    pass

这种方法将起作用,并允许我从许多类似功能的实例中考虑验证行为。但是现在我想写一个类方法,它也需要一个经过验证的URL。但是,幼稚的方法行不通

class SomeClass:
    @validate_url
    def some_method(self, url, some_other_args):
        pass

因为我们最终将尝试验证self而不是url。我的问题是如何编写单个装饰器,以最少的样板量实现对函数和方法的作用。

注1:我知道为什么发生这种情况,只是我不知道如何以最优雅的方式解决此问题。

注意2:URL验证问题只是一个例子,因此检查isinstance(args[0], str)是否不是一个好的解决方案。

1 个答案:

答案 0 :(得分:1)

One solution would be to somehow detect whether the decorated function is a class method or not — which seems to be difficult if not impossible (as far as I can tell anyway) to do so cleanly. The inspect module's ismethod() and isfunction() don't work inside a decorator used inside a class definition.

Given that, here's a somewhat hacky way of doing it which checks to see if the decorated callable's first argument has been given the name "self", which is the coding convention for it in class methods (although it is not a requirement, so caveat emptor and use at your own risk).

The following code seems to work in both Python 2 and 3. However in Python 3 it may raise DeprecationWarnings depending on exactly what sub-version is being used—so they have been suppressed in a section of the code below.

from functools import wraps
import inspect
import warnings

def validate_url(f):
    @wraps(f)
    def validated(*args, **kwargs):
        with warnings.catch_warnings():
            # Suppress DeprecationWarnings in this section.
            warnings.simplefilter('ignore', category=DeprecationWarning)

            # If "f"'s first argument is named "self",
            # assume it's a method.
            if inspect.getargspec(f).args[0] == 'self':
                url = args[1]
            else:  # Otherwise assume "f" is a ordinary function.
                url = args[0]
        print('testing url: {!r}'.format(url))
        assert len(url.split('.')) == 3  # Trivial "validation".
        return f(*args, **kwargs)
    return validated

@validate_url
def some_func(url, some_other_arg, *some_args, **some_kwargs):
    print('some_func() called')


class SomeClass:
    @validate_url
    def some_method(self, url, some_other_args):
        print('some_method() called')


if __name__ == '__main__':
    print('** Testing decorated function **')
    some_func('xxx.yyy.zzz', 'another arg')
    print('  URL OK')
    try:
        some_func('https://bogus_url.com', 'another thing')
    except AssertionError:
        print('  INVALID URL!')

    print('\n** Testing decorated method **')
    instance = SomeClass()
    instance.some_method('aaa.bbb.ccc', 'something else')  # -> AssertionError
    print('  URL OK')
    try:
        instance.some_method('foo.bar', 'arg 2')  # -> AssertionError
    except AssertionError:
        print('  INVALID URL!')

Output:

** Testing decorated function **
testing url: 'xxx.yyy.zzz'
some_func() called
  URL OK
testing url: 'https://bogus_url.com'
  INVALID URL!

** Testing decorated method **
testing url: 'aaa.bbb.ccc'
some_method() called
  URL OK
testing url: 'foo.bar'
  INVALID URL!