【鼎革‧革鼎】︰ Raspbian Stretch 《六之 J.2B 》

閱讀 Michele Simionato 先生寫作之

The decorator module

Author: Michele Simionato
E-mail: michele.simionato@gmail.com
Version: 4.1.1 (2017-07-16)
Supports: Python 2.6, 2.7, 3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6
Download page: http://pypi.python.org/pypi/decorator/4.1.1
Installation: pip install decorator
License: BSD license

說明文件︰

Introduction

The decorator module is over ten years old, but still alive and kicking. It is used by several frameworks (IPython, scipy, authkit, pylons, pycuda, sugar, …) and has been stable for a long time. It is your best option if you want to preserve the signature of decorated functions in a consistent way across Python releases. Version 4 is fully compatible with the past, except for one thing: support for Python 2.4 and 2.5 has been dropped. That decision made it possible to use a single code base both for Python 2.X and Python 3.X. This is a huge bonus, since I could remove over 2,000 lines of duplicated documentation/doctests. Having to maintain separate docs for Python 2 and Python 3 effectively stopped any development on the module for several years. Moreover, it is now trivial to distribute the module as an universal wheel since 2to3 is no more required. Since Python 2.5 has been released 9 years ago, I felt that it was reasonable to drop the support for it. If you need to support ancient versions of Python, stick with the decorator module version 3.4.2. The current version supports all Python releases from 2.6 up to 3.6.

What’s New in version 4

  • New documentation There is now a single manual for all Python versions, so I took the opportunity to overhaul the documentation. So, even if you are a long-time user, you may want to revisit the docs, since several examples have been improved.
  • Packaging improvements The code is now also available in wheel format. Integration with setuptools has improved and you can run the tests with the command python setup.py test too.
  • Code changes A new utility function decorate(func, caller) has been added. It does the same job that was performed by the older decorator(caller, func). The old functionality is now deprecated and no longer documented, but still available for now.
  • Multiple dispatch The decorator module now includes an implementation of generic functions (sometimes called “multiple dispatch functions”). The API is designed to mimic functools.singledispatch (added in Python 3.4), but the implementation is much simpler. Moreover, all decorators involved preserve the signature of the decorated functions. For now, this exists mostly to demonstrate the power of the module. In the future it could be enhanced/optimized; however, its API could change. (Such is the fate of experimental features!) In any case, it is very short and compact (less then 100 lines), so you can extract it for your own use. Take it as food for thought.
  • Python 3.5 coroutines From version 4.1 it is possible to decorate coroutines, i.e. functions defined with the async def syntax, and to maintain the inspect.iscoroutinefunction check working for the decorated function.

 

讓人眼界大開,了解『裝飾子』之用處︰

Usefulness of decorators

Python decorators are an interesting example of why syntactic sugar matters. In principle, their introduction in Python 2.4 changed nothing, since they did not provide any new functionality which was not already present in the language. In practice, their introduction has significantly changed the way we structure our programs in Python. I believe the change is for the best, and that decorators are a great idea since:

  • decorators help reducing boilerplate code;
  • decorators help separation of concerns;
  • decorators enhance readability and maintenability;
  • decorators are explicit.

Still, as of now, writing custom decorators correctly requires some experience and it is not as easy as it could be. For instance, typical implementations of decorators involve nested functions, and we all know that flat is better than nested.

The aim of the decorator module it to simplify the usage of decorators for the average programmer, and to popularize decorators by showing various non-trivial examples. Of course, as all techniques, decorators can be abused (I have seen that) and you should not try to solve every problem with a decorator, just because you can.

You may find the source code for all the examples discussed here in the documentation.py file, which contains the documentation you are reading in the form of doctests.

Definitions

Technically speaking, any Python object which can be called with one argument can be used as a decorator. However, this definition is somewhat too large to be really useful. It is more convenient to split the generic class of decorators in two subclasses:

signature-preserving decorators:
Callable objects which accept a function as input and return a function as output, with the same signature.
signature-changing decorators:
Decorators which change the signature of their input function, or decorators that return non-callable objects.

Signature-changing decorators have their use: for instance, the builtin classes staticmethod and classmethod are in this group. They take functions and return descriptor objects which are neither functions, nor callables.

Still, signature-preserving decorators are more common, and easier to reason about. In particular, they can be composed together, whereas other decorators generally cannot.

Writing signature-preserving decorators from scratch is not that obvious, especially if one wants to define proper decorators that can accept functions with any signature. A simple example will clarify the issue.

 

而且點出不留心、可能生潛在問題︰

Statement of the problem

A very common use case for decorators is the memoization of functions. A memoize decorator works by caching the result of the function call in a dictionary, so that the next time the function is called with the same input parameters the result is retrieved from the cache and not recomputed.

There are many implementations of memoize in http://www.python.org/moin/PythonDecoratorLibrary, but they do not preserve the signature. In recent versions of Python you can find a sophisticated lru_cache decorator in the standard library’s functools. Here I am just interested in giving an example.

Consider the following simple implementation (note that it is generally impossible to correctly memoize something that depends on non-hashable arguments):

def memoize_uw(func):
    func.cache = {}

    def memoize(*args, **kw):
        if kw:  # frozenset is used to ensure hashability
            key = args, frozenset(kw.items())
        else:
            key = args
        if key not in func.cache:
            func.cache[key] = func(*args, **kw)
        return func.cache[key]
    return functools.update_wrapper(memoize, func)

 

Here I used the functools.update_wrapper utility, which was added in Python 2.5 to simplify the writing of decorators. (Previously, you needed to manually copy the function attributes __name__, __doc__, __module__, and __dict__ to the decorated function by hand).

Here is an example of usage:

@memoize_uw
def f1(x):
    "Simulate some long computation"
    time.sleep(1)
    return x

 

This works insofar as the decorator accepts functions with generic signatures. Unfortunately, it is not a signature-preserving decorator, since memoize_uw generally returns a function with a different signature from the original.

Consider for instance the following case:

@memoize_uw
def f1(x):
    "Simulate some long computation"
    time.sleep(1)
    return x

 

Here, the original function takes a single argument named x, but the decorated function takes any number of arguments and keyword arguments:

>>> from decorator import getargspec  # akin to inspect.getargspec
>>> print(getargspec(f1))
ArgSpec(args=[], varargs='args', varkw='kw', defaults=None)

 

This means that introspection tools (like pydoc) will give false information about the signature of f1 – unless you are using Python 3.5. This is pretty bad: pydoc will tell you that the function accepts the generic signature *args, **kw, but calling the function with more than one argument raises an error:

>>> f1(0, 1)
Traceback (most recent call last):
   ...
TypeError: f1() takes exactly 1 positional argument (2 given)

 

Notice that inspect.getargspec and inspect.getfullargspec will give the wrong signature. This even occurs in Python 3.5, although both functions were deprecated in that release.

※ 參考

pi@raspberrypi:~ $ python3
Python 3.5.3 (default, Jan 19 2017, 14:11:04) 
[GCC 6.3.0 20170124] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import functools
>>> import time
>>> 
>>> def memoize_uw(func):
...     func.cache = {}
...     def memoize(*args, **kw):
...         if kw:  # frozenset is used to ensure hashability
...             key = args, frozenset(kw.items())
...         else:
...             key = args
...         if key not in func.cache:
...             func.cache[key] = func(*args, **kw)
...         return func.cache[key]
...     return functools.update_wrapper(memoize, func)
... 
>>> @memoize_uw
... def f1(x):
...     "Simulate some long computation"
...     time.sleep(1)
...     return x
... 
>>> f1(3)
3
>>> from decorator import getargspec
>>> print(getargspec(f1))
ArgSpec(args=[], varargs='args', varkw='kw', defaults=None)
>>> 
>>> f1(0,1)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 9, in memoize
TypeError: f1() takes 1 positional argument but 2 were given
>>> 
>>> import inspect
>>> print(inspect.getargspec(f1))
ArgSpec(args=[], varargs='args', keywords='kw', defaults=None)
>>> 

 

派生程式學習者或該反思

PEP 362 — Function Signature Object

Abstract

Python has always supported powerful introspection capabilities, including introspecting functions and methods (for the rest of this PEP, “function” refers to both functions and methods). By examining a function object you can fully reconstruct the function’s signature. Unfortunately this information is stored in an inconvenient manner, and is spread across a half-dozen deeply nested attributes.

This PEP proposes a new representation for function signatures. The new representation contains all necessary information about a function and its parameters, and makes introspection easy and straightforward.

However, this object does not replace the existing function metadata, which is used by Python itself to execute those functions. The new metadata object is intended solely to make function introspection easier for Python programmers.

Signature Object

A Signature object represents the call signature of a function and its return annotation. For each parameter accepted by the function it stores a Parameter object in its parameters collection.

A Signature object has the following public attributes and methods:

  • return_annotation : object

    The “return” annotation for the function. If the function has no “return” annotation, this attribute is set to Signature.empty.

  • parameters : OrderedDict

    An ordered mapping of parameters’ names to the corresponding Parameter objects.

  • bind(*args, **kwargs) -> BoundArguments

    Creates a mapping from positional and keyword arguments to parameters. Raises a TypeError if the passed arguments do not match the signature.

  • bind_partial(*args, **kwargs) -> BoundArguments

    Works the same way as bind(), but allows the omission of some required arguments (mimics functools.partial behavior.) Raises a TypeError if the passed arguments do not match the signature.

  • replace(parameters=<optional>, *, return_annotation=<optional>) -> Signature

    Creates a new Signature instance based on the instance replace was invoked on. It is possible to pass different parameters and/or return_annotation to override the corresponding properties of the base signature. To remove return_annotation from the copied Signature, pass in Signature.empty.

    Note that the ‘=<optional>’ notation, means that the argument is optional. This notation applies to the rest of this PEP.

 

的內容!評估實踐之反省?

29.12.3. Introspecting callables with the Signature object

New in version 3.3.

The Signature object represents the call signature of a callable object and its return annotation. To retrieve a Signature object, use the signature() function.

inspect.signature(callable, *, follow_wrapped=True)
Return a Signature object for the given callable:
>>> from inspect import signature
>>> def foo(a, *, b:int, **kwargs):
...     pass

>>> sig = signature(foo)

>>> str(sig)
'(a, *, b:int, **kwargs)'

>>> str(sig.parameters['b'])
'b:int'

>>> sig.parameters['b'].annotation
<class 'int'>

 

Accepts a wide range of python callables, from plain functions and classes to functools.partial() objects.

Raises ValueError if no signature can be provided, and TypeError if that type of object is not supported.

New in version 3.5: follow_wrapped parameter. Pass False to get a signature of callable specifically (callable.__wrapped__ will not be used to unwrap decorated callables.)

Note

Some callables may not be introspectable in certain implementations of Python. For example, in CPython, some built-in functions defined in C provide no metadata about their arguments.