跳转到主要内容

简单的Python装饰器

项目描述

https://travis-ci.org/mplanchard/pydecor.svg?branch=master https://readthedocs.org/projects/pydecor/badge/?version=latest

简单的Python装饰器!

摘要

装饰器很棒,但编写起来很困难,尤其是如果您想为装饰器添加参数,或者想在类方法和函数上使用装饰器。我知道,无论我写多少,我仍然每次都要查找语法。这仅仅是对简单的函数装饰器而言。在类和方法级别上使装饰器一致工作是一整个难题。

PyDecor旨在使函数装饰变得简单直接,这样开发者就可以停止担心闭包和三重嵌套函数中的语法,而是专注于装饰!

快速入门

安装 pydecor

pip install pydecor

使用其中一个现成的装饰器

# Memoize a function

from pydecor import memoize


@memoize()
def fibonacci(n):
    """Compute the given number of the fibonacci sequence"""
    if n < 2:
        return n
    return fibonacci(n - 2) + fibonacci(n - 1)

print(fibonacci(150))
# Intercept an error and raise a different one

from flask import Flask
from pydecor import intercept
from werkzeug.exceptions import InternalServerError


app = Flask(__name__)


@app.route('/')
@intercept(catch=Exception, reraise=InternalServerError,
           err_msg='The server encountered an error rendering "some_view"')
def some_view():
    """The root view"""
    assert False
    return 'Asserted False successfully!'


client = app.test_client()
response = client.get('/')

assert response.status_code == 500
assert 'some_view'.encode() in resp.data

使用通用装饰器运行自己的函数 @before@after@instead 而不是另一个函数,如下面的例子所示,它为 Flask 响应设置了 User-Agent 标头

 from flask import Flask, make_response
 from pydecor import Decorated, after


 app = Flask(__name__)

# `Decorated` instances are passed to your functions and contain
# information about the wrapped function, including its `args`,
# `kwargs`, and `result`, if it's been called.

 def set_user_agent(decorated: Decorated):
     """Sets the user-agent header on a result from a view"""
     resp = make_response(decorated.result)
     resp.headers.set('User-Agent', 'my_applicatoin')
     return resp


 @app.route('/')
 @after(set_user_agent)
 def index_view():
     return 'Hello, world!'


 client = app.test_client()
 response = client.get('/')
 assert response.headers.get('User-Agent') == 'my_application'

或者使用 construct_decorator 创建自己的装饰器

from flask import reques
from pydecor import Decorated, construct_decorator
from werkzeug.exceptions import Unauthorized


def check_auth(_decorated: Decorated, request):
    """Theoretically checks auth.

    It goes without saying, but this is example code. You should
    not actually check auth this way!
    """
    if request.host != 'localhost':
        raise Unauthorized('locals only!')


authed = construct_decorator(before=check_auth)


app = Flask(__name__)


@app.route('/')
# Any keyword arguments provided to any of the generic decorators are
# passed directly to your callable.
@authed(request=request)
def some_view():
    """An authenticated view"""
    return 'This is sensitive data!'

为什么选择PyDecor?

  • 很简单!

    使用 PyDecor,您可以从这样

    from functools import wraps
    from flask import request
    from werkzeug.exceptions import Unauthorized
    from my_pkg.auth import authorize_request
    
    def auth_decorator(request=None):
        """Check the passed request for authentication"""
    
        def decorator(decorated):
    
            @wraps(decorated)
            def wrapper(*args, **kwargs):
                if not authorize_request(request):
                  raise Unauthorized('Not authorized!')
                return decorated(*args, **kwargs)
            return wrapper
    
        return decorated
    
    @auth_decorator(request=requst)
    def some_view():
        return 'Hello, World!'

    变成这样

    from flask import request
    from pydecor import before
    from werkzeug.exceptions import Unauthorized
    from my_pkg.auth import authorize_request
    
    def check_auth(_decorated, request=request):
        """Ensure the request is authorized"""
        if not authorize_request(request):
          raise Unauthorized('Not authorized!')
    
    @before(check_auth, request=request)
    def some_view():
        return 'Hello, world!'

    这不仅代码更少,而且您不需要记住装饰器语法或处理嵌套函数。完全坦白,我不得不查找装饰器示例来确保我正确地得到了第一个示例的语法,我刚刚花了两周时间编写装饰器库。

  • 很快!

    PyDecor 旨在让您的生命更轻松,而不是更慢。装饰机机制被设计得尽可能高效,欢迎贡献来加快速度。

  • 隐式方法装饰!

    当应用于类时,让装饰器“滚下”到方法是一个复杂的过程,但 PyDecor 的所有装饰器都免费提供这项服务,因此您不必编写

    from pydecor import log_call
    
    class FullyLoggedClass(object):
    
        @log_call(level='debug')
        def some_function(self, *args, **kwargs):
            return args, kwargs
    
        @log_call(level='debug')
        def another_function(self, *args, **kwargs):
            return None
    
        ...

    您只需编写

    from pydecor import log_call
    
    @log_call(level='debug')
    class FullyLoggedClass(object):
    
        def some_function(self, *args, **kwargs):
            return args, kwargs
    
        def another_function(self, *args, **kwargs):
            return None
    
        ...

    PyDecor 忽略特殊方法(如 __init__),以免干扰深层次的 Python 魔法。默认情况下,它适用于类的任何方法,包括实例、类和静态方法。它还确保在装饰后类属性得到保留,因此您的类引用继续按照预期行为。

  • 一致的方法装饰!

    无论您是在装饰类、实例方法、类方法还是静态方法,您都可以使用同一个传递的函数。从传递给提供的可调用的方法参数中删除了 selfcls 变量,因此您的函数不需要关心它们被用于何处。

  • 大量测试!

    当真。你不信?看看。我们拥有最好的测试。简直太棒了。

安装

`pydecor` 2.0 及以后版本仅支持 Python 3.6+!

如果您需要支持较老的 Python,请使用最新的 1.x 版本。

要安装 pydecor,只需运行

pip install -U pydecor

要安装当前的开发版本

pip install --pre -U pydecor

您还可以从源安装以获取最新的代码,这可能或可能不适用于功能

git clone https://github.com/mplanchard/pydecor
pip install ./pydecor

详情

提供的装饰器

此软件包提供通用装饰器,可以与任何函数一起使用,为装饰的资源提供额外的实用功能,以及现成的(现成的)装饰器以立即使用。

以下信息足以让您开始,但我强烈建议查看 装饰器模块文档 以了解各种装饰器的所有选项和详细信息!

泛型

  • @before - 在装饰函数执行之前运行可调用对象

    • 使用 Decorated 的实例和任何提供的 kwargs 调用

  • @after - 在装饰函数执行之后运行可调用对象

    • 使用 Decorated 的实例和任何提供的 kwargs 调用

  • @instead - 在装饰函数的位置运行可调用对象

    • 使用 Decorated 的实例和任何提供的 kwargs 调用

  • @decorate - 指定在装饰函数之前、之后和/或其位置运行的多个可调用对象

    • 传递给 decoratebeforeafterinstead 关键字参数的可调用对象将与上面描述的各个装饰器相同的默认函数签名一起调用。所有提供可调用对象都将传递额外的参数。

  • construct_decorator - 指定在装饰函数之前、之后或代替装饰函数运行的函数。返回一个可重用的装饰器。

传递给通用装饰器的可调用对象预期至少处理一个位置参数,该参数将是 Decorated 实例。 Decorated 对象提供以下接口

属性

  • args:一个元组,包含调用装饰的可调用对象时的任何位置参数

  • kwargs:一个字典,包含调用装饰的可调用对象时的任何关键字参数

  • wrapped:对装饰的可调用对象的引用

  • result:当调用 _wrapped_ 函数时,其返回值将存储在此处

方法

  • __call__(*args, **kwargs):一个快捷方式到 decorated.wrapped(*args, **kwargs),调用 Decorated 实例调用底层包装的可调用对象。此调用(或直接调用 decorated.wrapped())的结果将设置 result 属性。

每个通用装饰器都可以接受任何数量的关键字参数,这些参数将直接传递给提供可调用对象,因此,运行下面的代码会打印“red”

from pydecor import before

def before_func(_decorated, label=None):
    print(label)

@before(before_func, label='red')
def red_function():
    pass

red_function()

每个通用装饰器接受以下关键字参数

  • implicit_method_decoration - 如果为 True,则装饰类意味着装饰其所有方法。 注意:除非你了解自己在做什么,否则你可能希望保留此设置。

  • instance_methods_only - 如果为 True,则当 implicit_method_decoration 为 True 时,只有实例方法(而不是类或静态方法)将自动被装饰

可以使用 construct_decorator 函数将 @before@after@instead 调用组合到一个装饰器中,而不必担心意外的堆叠效果。让我们创建一个装饰器,当函数开始和结束时发出通知

from pydecor import construct_decorator

def before_func(decorated):
    print('Starting decorated function '
          '"{}"'.format(decorated.wrapped.__name__))

def after_func(decorated):
    print('"{}" gave result "{}"'.format(
        decorated.wrapped.__name__, decorated.result
    ))

my_decorator = construct_decorator(before=before_func, after=after_func)

@my_decorator()
def this_function_returns_nothing():
    return 'nothing'

输出结果?

Starting decorated function "this_function_returns_nothing"
"this_function_returns_nothing" gave result "nothing"

可能一个更现实的例子会有用。假设我们想在 Flask 响应中添加标题。

from flask import Flask, Response, make_response
from pydecor import construct_decorator


def _set_app_json_header(decorated):
    # Ensure the response is a Response object, even if a tuple was
    # returned by the view function.
    response = make_response(decorated.result)
    response.headers.set('Content-Type', 'application/json')
    return response


application_json = construct_decorator(after=_set_app_json_header)


# Now you can decorate any Flask view, and your headers will be set.

app = Flask(__name__)

# Note that you must decorate "before" (closer to) the function than the
# app.route() decoration, because the route decorator must be called on
# the "finalized" version of your function

@app.route('/')
@application_json()
def root_view():
    return 'Hello, world!'

client = app.test_client()
response = app.get('/')

print(response.headers)

输出结果?

..code

Content-Type: application/json
Content-Length: 13

现成的(成衣)

  • export - 将装饰的类或函数添加到其模块的 __all__ 列表中,将其公开为“公共”引用。

  • intercept - 捕获指定的异常,并且可以选择重新抛出和/或调用提供的回调来处理异常

  • log_call - 自动记录装饰函数的调用签名和结果

  • memoize - 缓存函数的调用和返回值以供重用。可以使用 pydecor.caches 中的任何缓存,这些缓存都有选项来自动修剪,以防止缓存过大。

缓存

pydecor 提供了三个缓存。这些缓存设计用于传递给 @memoization 装饰器,如果您想使用除默认 LRUCache 之外的内容,但它们在其他地方也是完全有效的。

所有缓存都实现了标准字典接口。

LRUCache

一个最少使用缓存。获取和设置键值对的结果是它们被考虑为最近最少使用的。当缓存达到指定的 max_size 时,最近最少使用的项将被丢弃。

FIFOCache

一个先入先出缓存。当缓存达到指定的 max_size 时,首先插入的项将被丢弃,然后是第二个,依此类推。

TimedCache

一种条目会过期的缓存。如果指定了 max_age,则任何超过 max_age(以秒为单位)的条目将被视为无效,并在访问时被删除。

堆叠

通用和现成的装饰器可以堆叠!您可以堆叠多个相同的装饰器,也可以混合搭配。以下是一些需要注意的问题。

通常,堆叠的方式与您预期的相同,但在使用 @instead 装饰器或使用 @instead 作为内部工作的 @intercept 装饰器时,需要注意一些细节。

只需记住,@instead 会替换所有在它之前的内容。因此,只要 @instead 调用了装饰过的函数,堆叠它是可以的。在这种情况下,它将在下面指定的任何装饰器之前被调用,并且那些装饰器将在它调用装饰过的函数时执行。@intercept 也会以这种方式表现。

如果 @instead 装饰器没有调用装饰过的函数,而是完全替换它,则它 必须 是第一个指定的(堆叠装饰器的底部),否则下面的装饰器将不会执行。

对于 @before@after,装饰器的指定顺序并不重要。@before 总是首先被调用,而 @after 最后。

类装饰

类装饰器可能很复杂,但 PyDecor 旨在使其尽可能简单直观!

默认情况下,装饰类会将该装饰器应用于该类中所有的方法(实例、类和静态)。装饰器会应用于类和静态方法,无论它们是通过实例还是通过类引用来引用。在类级别指定的“额外”内容在调用不同方法之间持续存在,允许像类级别缓存字典这样的功能(测试套件中有一个非常基本的测试演示了这种模式)。

如果您希望装饰器不应用于类和静态方法,则在装饰类时设置 instance_methods_only=True

如果您想装饰类本身而不是其方法,请注意,装饰器将在类实例化时触发,并且如果装饰器替换或修改了返回值,则该返回值将替换实例化的类。考虑到这些限制,在装饰类时设置 implicit_method_decoration=False 可以启用该功能。

方法装饰

装饰器也可以直接应用于静态、类或实例方法。如果与 @staticmethod@classmethod 装饰器结合使用,那些装饰器应该始终位于装饰器堆栈的“顶部”(最远离函数的位置)。

在装饰实例方法时,将 self 从传递给提供的可调用的参数中删除。

在装饰类方法时,将 cls 从传递给提供的可调用的参数中删除。

目前,类的引用和实例引用不需要分别命名为"cls""self"即可被移除。然而,这并不保证在未来的版本中仍然如此,因此如果可能的话,请尽量保持您的命名标准(只是了解一下,"self"更有可能被要求)。

示例

以下是一些通用和标准装饰器的示例。请查看API文档以获取更多信息,并查看方便的装饰器,这些都是使用本库中的beforeafterinstead装饰器实现的。

更新函数的参数或关键字参数

传递给@before的函数可以返回None,在这种情况下,装饰函数的参数不会发生任何变化,或者它们可以返回一个元组,包含参数(作为一个元组)和关键字参数(作为一个字典),在这种情况下,这些参数将用于装饰函数。在这个例子中,我们简化了一个非常严肃的函数。

from pydecor import before

def spamify_func(decorated):
    """Mess with the function arguments"""
    args = tuple(['spam' for _ in decorated.args])
    kwargs = {k: 'spam' for k in decorated.kwargs}
    return args, kwargs


@before(spamify_func)
def serious_function(serious_string, serious_kwarg='serious'):
    """A very serious function"""
    print('A serious arg: {}'.format(serious_string))
    print('A serious kwarg: {}'.format(serious_kwarg))

serious_function('Politics', serious_kwarg='Religion')

输出结果?

A serious arg: spam
A serious kwarg: spam

对函数的返回值进行操作

传递给@after的函数将装饰函数的返回值作为Decorated实例的一部分接收。如果@after返回None,则返回值将保持不变发送回去。然而,如果@after返回某些内容,则其返回值将作为函数的返回值发送回去。

在这个例子中,我们确保函数的返回值已经被彻底的垃圾邮件化。

from pydecor import after

def spamify_return(decorated):
    """Spamify the result of a function"""
    return 'spam-spam-spam-spam-{}-spam-spam-spam-spam'.format(decorated.result)


@after(spamify_return)
def unspammed_function():
    """Return a non-spammy value"""
    return 'beef'

print(unspammed_function())

输出结果?

spam-spam-spam-spam-beef-spam-spam-spam-spam

替换函数

传递给@instead的函数也通过Decorated对象提供了包装的上下文。但是,如果instead可调用未调用包装函数,则它根本不会被调用。也许当某个条件为真时,你想跳过函数,但又不想使用pytest.skipif,因为出于某种原因,pytest不能成为你的生产代码的依赖项。

from pydecor import instead

def skip(decorated, when=False):
    if when:
        pass
    else:
        # Calling `decorated` calls the wrapped function.
        return decorated(*decorated.args, **decorated.kwargs)


@instead(skip, when=True)
def uncalled_function():
    print("You won't see me (you won't see me)")


uncalled_function()

输出结果?

(没有输出,因为函数被跳过了)

自动记录函数调用和结果

也许你想要确保你的函数被记录下来,而无需每次都烦恼于处理日志的模板。@log_call试图自动获取与装饰发生模块对应的日志实例(就像你调用logging.getLogger(__name__)一样),或者你可以传递你自己的,花哨的,定制的,带提示的日志实例。

from logging import getLogger, StreamHandler
from sys import stdout

from pydecor import log_call


# We're just getting a logger here so we can see the output. This isn't
# actually necessary for @log_call to work!
log = getLogger(__name__)
log.setLevel('DEBUG')
log.addHandler(StreamHandler(stdout))


@log_call()
def get_lucky(*args, **kwargs):
    """We're up all night 'till the sun."""
    return "We're up all night for good fun."


get_lucky('Too far', 'to give up', who_we='are')

输出结果?

get_lucky(*('Too far', 'to give up'), **{'who_we': 'are'}) -> "We're up all night for good fun"

拦截异常并重新抛出自定义异常

你是否是一位被压得喘不过气的库开发者,厌倦了不断重新抛出自定义异常,以便你的库用户可以有一个寻找基础异常的漂亮的try/except?让我们让它更容易一些

from pydecor import intercept


class BetterException(Exception):
    """Much better than all those other exceptions"""


@intercept(catch=RuntimeError, reraise=BetterException)
def sometimes_i_error(val):
    """Sometimes, this function raises an exception"""
    if val > 5:
        raise RuntimeError('This value is too big!')


for i in range(7):
    sometimes_i_error(i)

输出结果?

Traceback (most recent call last):
  File "/Users/Nautilus/Library/Preferences/PyCharm2017.1/scratches/scratch_1.py", line 88, in <module>
    sometimes_i_error(i)
  File "/Users/Nautilus/Documents/Programming/pydecor/pydecor/decorators.py", line 389, in wrapper
    return fn(**fkwargs)
  File "/Users/Nautilus/Documents/Programming/pydecor/pydecor/functions.py", line 58, in intercept
    raise_from(new_exc, context)
  File "<string>", line 2, in raise_from
__main__.BetterException: This value is too big!

拦截异常,执行某些操作,然后重新抛出原始异常

也许你想抛出自定义异常。也许原始的一个就很好。你只想在重新抛出原始异常之前打印一条特殊消息。PyDecor为你提供了保障

from pydecor import intercept


def print_exception(exc):
    """Make sure stdout knows about our exceptions"""
    print('Houston, we have a problem: {}'.format(exc))


@intercept(catch=Exception, handler=print_exception, reraise=True)
def assert_false():
    """All I do is assert that False is True"""
    assert False, 'Turns out, False is not True'


assert_false()

并且输出

Houston, we have a problem: Turns out, False is not True
Traceback (most recent call last):
  File "/Users/Nautilus/Library/Preferences/PyCharm2017.1/scratches/scratch_1.py", line 105, in <module>
    assert_false()
  File "/Users/Nautilus/Documents/Programming/pydecor/pydecor/decorators.py", line 389, in wrapper
    return fn(**fkwargs)
  File "/Users/Nautilus/Documents/Programming/pydecor/pydecor/functions.py", line 49, in intercept
    return decorated(*decorated_args, **decorated_kwargs)
  File "/Users/Nautilus/Library/Preferences/PyCharm2017.1/scratches/scratch_1.py", line 102, in assert_false
    assert False, 'Turns out, False is not True'
AssertionError: Turns out, False is not True

拦截异常,处理并完成

有时异常并不是世界末日,并且不需要冒泡到你的应用程序的顶部。在这些情况下,也许只是处理它,不要重新抛出

from pydecor import intercept


def let_us_know_it_happened(exc):
    """Just let us know an exception happened (if we are reading stdout)"""
    print('This non-critical exception happened: {}'.format(exc))


@intercept(catch=ValueError, handler=let_us_know_it_happened)
def resilient_function(val):
    """I am so resilient!"""
    val = int(val)
    print('If I get here, I have an integer: {}'.format(val))


resilient_function('50')
resilient_function('foo')

输出

If I get here, I have an integer: 50
This non-critical exception happened: invalid literal for int() with base 10: 'foo'

请注意,在处理异常后,函数不会继续运行。使用此方法在某些条件下进行短路,而不是用于建立try/except:pass块。也许有一天我会想出如何让它像那样工作,但就目前而言,装饰器包围了整个函数,因此它不提供那么精细的控制级别。

路线图

2.?

更多现成的装饰器

  • skipif - 与py.test的装饰器类似,如果提供的条件为真,则跳过函数

请告诉我如果您有任何其他装饰器的想法,这将是非常受欢迎的!

类型注解

由于我们已经停止了对Python 2的支持,我们可以使用类型注解来正确地注解函数的输入和返回值,并使它们对库作者可用。

贡献

欢迎贡献!如果您发现了一个错误或者某些东西没有按照您预期的那样工作,请提出问题。如果您知道如何修复错误,请打开一个PR

我绝对欢迎任何形式的贡献。如果您认为文档可以改进,或者您发现了错别字,请打开一个PR来改进和/或修复它们。

贡献者行为准则

有一个包含详细信息的CODE_OF_CONDUCT.md文件,基于GitHub的模板,但关键是,我期望所有为这个项目做出贡献的人都能尽最大努力做到有帮助、友好和耐心。任何形式的歧视都将不会容忍,并将立即报告给GitHub。

就个人而言,开源之所以能够生存,是因为那些愿意免费贡献时间和精力的人。我们至少要尊重他们。

测试

可以使用以下命令运行测试:

make test

这将使用您本地的python3。如果您有其他Python版本可用,您可以运行

make tox

来尝试在所有支持的Python版本上本地运行。

如果您已安装Docker,您可以运行

make test-docker-{version}  # e.g. make test-docker-3.6

来拉取适当的Docker镜像并在其中运行测试。您也可以运行

make test-docker

为此运行所有支持的Python版本的测试。

导致测试失败的PR将不会合并,直到测试通过。

任何新的功能都应附带适当的测试。如果您有任何问题,请随时通过电子邮件至msplanchard @ gmail 或通过GH的Issues与我联系。

自动格式化

本项目使用black进行自动格式化。我建议将编辑器设置为在此项目中保存时格式化,但您也可以运行

make fmt

来格式化所有内容。

代码风格检查

可以使用以下命令运行linting:

make lint

目前,linting验证以下内容:

  • 无flake8错误

  • 无mypy错误

  • 无pylint错误

  • 无需要格式化的文件

在打开PR之前,您应该确保make lint返回0。

文档

文档通过Sphinx自动生成。您可以通过运行以下命令在本地构建它们:

make docs

然后,您可以在您选择的网页浏览器中打开docs/_build/html/index.html来查看您的更改后的文档将如何显示。

部署

部署通过推送标签处理。任何推送的标签如果当前版本尚未在PyPI上存在,都会导致向PyPI推送。

通过使用以下命令可以简化推送适当的标签:

VERSION=1.0.0 make distribute

其中VERSION显然应该是当前版本。这将验证指定的版本与软件包的当前版本匹配,确保最新的master正在分发,提示输入消息,并创建并推送一个格式为v{version}的签名标签。

项目详情


下载文件

下载适用于您平台文件的文件。如果您不确定选择哪个,请了解有关安装包的更多信息。

源代码分发

pydecor-2.0.1.tar.gz (63.8 kB 查看哈希值)

上传时间 源代码

构建分发

pydecor-2.0.1-py3-none-any.whl (27.0 kB 查看哈希值)

上传时间 Python 3

支持者