Maison > développement back-end > Tutoriel Python > Comment étendre la fonctionnalité du minuteur en Python à l'aide de décorateurs ?

Comment étendre la fonctionnalité du minuteur en Python à l'aide de décorateurs ?

WBOY
Libérer: 2023-05-08 19:04:16
avant
1429 Les gens l'ont consulté

    Introduction

    Supposons que nous devions suivre le temps passé dans une fonction donnée dans notre base de code. En utilisant le gestionnaire de contexte, il existe essentiellement deux options différentes :

    1. Utiliser Timer à chaque fois que la fonction est appelée :

    with Timer("some_name"):
        do_something()
    Copier après la connexion

    Lorsque nous appelons la fonction do_something() plusieurs fois dans un fichier py, alors cela le fera. peut devenir très fastidieux et difficile à entretenir.

    2. Enveloppez le code dans une fonction dans le gestionnaire de contexte :

    def do_something():
        with Timer("some_name"):
            ...
    Copier après la connexion

    Timer ne doit être ajouté qu'à un seul endroit, mais cela entraînera do_something()</code > Augmente le niveau d'indentation de toute la définition. <code>Timer 只需要在一个地方添加,但这会为do_something()的整个定义增加一个缩进级别。

    更好的解决方案是使用 Timer 作为装饰器。装饰器是用于修改函数和类行为的强大构造。

    理解 Python 中的装饰器

    装饰器是包装另一个函数以修改其行为的函数。你可能会有疑问,这怎么实现呢?其实函数是 Python 中的first-class 对象,换句话说,函数可以以变量的形式传递给其他函数的参数,就像任何其他常规对象一样。因此此处有较大的灵活性,也是 Python 几个最强大功能的基础。

    我们首先创建第一个示例,一个什么都不做的装饰器:

    def turn_off(func):
        return lambda *args, **kwargs: None
    Copier après la connexion

    首先注意这个turn_off()只是一个常规函数。之所以成为装饰器,是因为它将一个函数作为其唯一参数并返回另一个函数。我们可以使用turn_off()来修改其他函数,例如:

    >>> print("Hello")
    Hello
    
    >>> print = turn_off(print)
    >>> print("Hush")
    >>> # Nothing is printed
    Copier après la connexion

    代码行 print = turn_off(print)turn_off() 装饰器装饰了 print 语句。实际上,它将函数 print() 替换为匿名函数 lambda *args, **kwargs: None 并返回 turn_off()匿名函数 lambda 除了返回 None 之外什么都不做。

    要定义更多丰富的装饰器,需要了解内部函数内部函数是在另一个函数内部定义的函数,它的一种常见用途是创建函数工厂:

    def create_multiplier(factor):
        def multiplier(num):
            return factor * num
        return multiplier
    Copier après la connexion

    multiplier() 是一个内部函数,在 create_multiplier() 内部定义。注意可以访问 multiplier() 内部的因子,而 multiplier()未在 create_multiplier() 外部定义:

    multiplier
    Copier après la connexion

    Traceback (most recent call last):
    File "", line 1, in
    NameError: name 'multiplier' is not defined

    相反,可以使用create_multiplier()创建新的 multiplier 函数,每个函数都基于不同的参数factor:

    double = create_multiplier(factor=2)
    double(3)
    Copier après la connexion

    6

    quadruple = create_multiplier(factor=4)
    quadruple(7)
    Copier après la connexion

    28

    同样,可以使用内部函数来创建装饰器。装饰器是一个返回函数的函数:

    def triple(func):
        def wrapper_triple(*args, **kwargs):
            print(f"Tripled {func.__name__!r}")
            value = func(*args, **kwargs)
            return value * 3
        return wrapper_triple
    Copier après la connexion

    triple() 是一个装饰器,因为它是一个期望函数 func() 作为其唯一参数并返回另一个函数 wrapper_triple() 的函数。注意 triple() 本身的结构:

    • 第 1 行开始了 triple() 的定义,并期望一个函数作为参数。

    • 第 2 到 5 行定义了内部函数 wrapper_triple()

    • 第 6 行返回 wrapper_triple()

    这是种定义装饰器的一般模式(注意内部函数的部分):

    • 第 2 行开始 wrapper_triple() 的定义。此函数将替换 triple() 修饰的任何函数。参数是 *args**kwargs,用于收集传递给函数的任何位置参数和关键字参数。我们可以灵活地在任何函数上使用 triple()

    • 第 3 行打印出修饰函数的名称,并指出已对其应用了 triple()

    • 第 4 行调用 func()triple() 修饰的函数。它传递传递给 wrapper_triple() 的所有参数。

    • 第 5 行func() 的返回值增加三倍并将其返回。

    接下来的代码中,knock()

    Une meilleure solution consiste à utiliser 🎜Timer comme décorateur🎜. Les décorateurs sont des constructions puissantes utilisées pour modifier le comportement des fonctions et des classes. 🎜🎜Comprendre les décorateurs en Python 🎜🎜🎜Un décorateur 🎜 est une fonction qui enveloppe une autre fonction pour modifier son comportement. Vous avez peut-être des questions, comment y parvenir ? En fait, les fonctions sont des objets de première classe en Python. En d'autres termes, les fonctions peuvent être transmises en tant que paramètres à d'autres fonctions sous forme de variables, comme n'importe quel autre objet normal. Il y a donc beaucoup de flexibilité ici, et c'est la base de certaines des fonctionnalités les plus puissantes de Python. 🎜🎜Nous commençons par créer notre premier exemple, un décorateur qui ne fait rien : 🎜
    >>> def knock():
    ...     return "Penny! "
    >>> knock = triple(knock)
    >>> result = knock()
    Tripled &#39;knock&#39;
    
    >>> result
    &#39;Penny! Penny! Penny! &#39;
    Copier après la connexion
    Copier après la connexion
    🎜Notez d'abord que ce turn_off() n'est qu'une fonction normale. C'est un décorateur car il prend une fonction comme seul argument et renvoie une autre fonction. On peut utiliser turn_off() pour modifier d'autres fonctions, par exemple : 🎜
    >>> @triple
    ... def knock():
    ...     return "Penny! "
    ...
    >>> result = knock()
    Tripled &#39;knock&#39;
    
    >>> result
    &#39;Penny! Penny! Penny! &#39;
    Copier après la connexion
    Copier après la connexion
    🎜La ligne de code print = turn_off(print) est agrémentée de turn_off() Le conteneur est décoré d'instructions print. En effet, il remplace la fonction print() par la fonction anonyme lambda *args, **kwargs: None et renvoie turn_off(). 🎜La fonction anonyme lambda ne fait rien sauf renvoyer Aucun. 🎜🎜🎜Pour définir des décorateurs plus riches, vous devez comprendre les 🎜fonctions internes🎜. 🎜Une fonction interne est une fonction définie à l'intérieur d'une autre fonction🎜 Une utilisation courante consiste à créer une fabrique de fonctions : 🎜
    knock
    Copier après la connexion
    Copier après la connexion
    🎜multiplier() est une fonction interne, dans create_multiplier(). Définition interne. Notez que les facteurs à l'intérieur de multiplier() sont accessibles, tandis que multiplier() n'est pas défini en dehors de create_multiplier() : 🎜
    <function triple.<locals>.wrapper_triple 
    at 0x7fa3bfe5dd90>
    Copier après la connexion
    Copier après la connexion
    🎜 Traceback (dernier appel le plus récent) :🎜 Fichier "", ligne 1, dans 🎜NameError : le nom 'multiplier' n'est pas défini🎜
    🎜À la place, vous pouvez utiliser create_multiplier () Créez de nouvelles fonctions multiplicateurs, chacune basée sur un facteur de paramètre différent : 🎜
    import functools
    
    def triple(func):
        @functools.wraps(func)
        def wrapper_triple(*args, **kwargs):
            print(f"Tripled {func.__name__!r}")
            value = func(*args, **kwargs)
            return value * 3
        return wrapper_triple
    Copier après la connexion
    Copier après la connexion
    🎜6🎜
    @triple
    def knock():
        return "Penny! "
    knock
    Copier après la connexion
    Copier après la connexion
    🎜28 🎜
    🎜De même, vous pouvez utiliser des fonctions internes créer un décorateur. Un décorateur est une fonction qui renvoie une fonction : 🎜
    <function knock at 0x7fa3bfe5df28>
    Copier après la connexion
    Copier après la connexion
    🎜triple() est un décorateur car c'est une fonction qui attend une fonction func() comme seul argument et renvoie une autre fonction A wrapper_triple(). Faites attention à la structure de triple() lui-même : 🎜
    • 🎜🎜La ligne 1🎜 commence le triple()</code > Définition, et attend une fonction en paramètre. 🎜</li><li>🎜🎜Les lignes 2 à 5🎜 définissent la fonction interne <code>wrapper_triple(). 🎜
    • 🎜🎜La ligne 6🎜 renvoie wrapper_triple(). 🎜
    • 🎜🎜Il s'agit d'un modèle général pour définir les décorateurs (notez la partie fonction interne) : 🎜
      • 🎜🎜Ligne 2🎜Démarrer Définition de wrapper_triple(). Cette fonction remplacera toute fonction modifiée par triple(). Les paramètres sont *args et **kwargs, qui collectent tous les arguments de position et de mot-clé transmis à la fonction. Nous avons la possibilité d'utiliser triple() sur n'importe quelle fonction. 🎜
      • 🎜🎜Ligne 3🎜 imprime le nom de la fonction modifiée et indique que triple() lui a été appliqué. 🎜
      • 🎜🎜Ligne 4🎜 appelle la fonction modifiée par func(), triple() . Il transmet tous les arguments passés à wrapper_triple(). 🎜
      • 🎜🎜Ligne 5🎜 triple la valeur de retour de func() et la renvoie. 🎜
      • 🎜🎜Dans le code suivant, knock() est une fonction qui renvoie le mot Penny. Passez-le à la fonction triple() et voyez quel est le résultat. 🎜
        >>> def knock():
        ...     return "Penny! "
        >>> knock = triple(knock)
        >>> result = knock()
        Tripled &#39;knock&#39;
        
        >>> result
        &#39;Penny! Penny! Penny! &#39;
        Copier après la connexion
        Copier après la connexion

        我们都知道,文本字符串与数字相乘,是字符串的一种重复形式,因此字符串 'Penny' 重复了 3 次。可以认为,装饰发生在knock = triple(knock)

        上述方法虽然实现了装饰器的功能,但似乎有点笨拙。PEP 318 引入了一种更方便的语法来应用装饰器。下面的 knock() 定义与上面的定义相同,但装饰器用法不同。

        >>> @triple
        ... def knock():
        ...     return "Penny! "
        ...
        >>> result = knock()
        Tripled &#39;knock&#39;
        
        >>> result
        &#39;Penny! Penny! Penny! &#39;
        Copier après la connexion
        Copier après la connexion

        @ 符号用于应用装饰器,@triple 表示 triple() 应用于紧随其后定义的函数。

        Python 标准库中定义的装饰器方法之一是:@functools.wraps。这在定义你自己的装饰器时非常有用。前面说过,装饰器是用另一个函数替换了一个函数,会给你的函数带来一个微妙的变化:

        knock
        Copier après la connexion
        Copier après la connexion
        <function triple.<locals>.wrapper_triple 
        at 0x7fa3bfe5dd90>
        Copier après la connexion
        Copier après la connexion

        @triple 装饰了 knock(),然后被 wrapper_triple() 内部函数替换,被装饰的函数的名字会变成装饰器函数,除了名称,还有文档字符串和其他元数据都将会被替换。但有时,我们并不总是想将被修饰的函数的所有信息都被修改了。此时 @functools.wraps 正好解决了这个问题,如下所示:

        import functools
        
        def triple(func):
            @functools.wraps(func)
            def wrapper_triple(*args, **kwargs):
                print(f"Tripled {func.__name__!r}")
                value = func(*args, **kwargs)
                return value * 3
            return wrapper_triple
        Copier après la connexion
        Copier après la connexion

        使用 @triple 的这个新定义保留元数据:

        @triple
        def knock():
            return "Penny! "
        knock
        Copier après la connexion
        Copier après la connexion
        <function knock at 0x7fa3bfe5df28>
        Copier après la connexion
        Copier après la connexion

        注意knock() 即使在被装饰之后,也同样保留了它的原有函数名称。当定义装饰器时,使用 @functools.wraps 是一种不错的选择,可以为大多数装饰器使用的如下模板:

        import functools
        
        def decorator(func):
            @functools.wraps(func)
            def wrapper_decorator(*args, **kwargs):
                # Do something before
                value = func(*args, **kwargs)
                # Do something after
                return value
            return wrapper_decorator
        Copier après la connexion

        创建 Python 定时器装饰器

        在本节中,云朵君将和大家一起学习如何扩展 Python 计时器,并以装饰器的形式使用它。接下来我们从头开始创建 Python 计时器装饰器。

        根据上面的模板,我们只需要决定在调用装饰函数之前和之后要做什么。这与进入和退出上下文管理器时的注意事项类似。在调用修饰函数之前启动 Python 计时器,并在调用完成后停止 Python 计时器。可以按如下方式定义 @timer 装饰器:

        import functools
        import time
        
        def timer(func):
            @functools.wraps(func)
            def wrapper_timer(*args, **kwargs):
                tic = time.perf_counter()
                value = func(*args, **kwargs)
                toc = time.perf_counter()
                elapsed_time = toc - tic
                print(f"Elapsed time: {elapsed_time:0.4f} seconds")
                return value
            return wrapper_timer
        Copier après la connexion

        可以按如下方式应用 @timer

        @timer
        def download_data():
            source_url = &#39;https://cloud.tsinghua.edu.cn/d/e1ccfff39ad541908bae/files/?p=%2Fall_six_datasets.zip&dl=1&#39;
            headers = {&#39;User-Agent&#39;: &#39;Mozilla/5.0&#39;}
            res = requests.get(source_url, headers=headers) 
        
        download_data()
        # Python Timer Functions: Three Ways to Monitor Your Code
        Copier après la connexion

        [ ... ]
        Elapsed time: 0.5414 second

        回想一下,还可以将装饰器应用于先前定义的下载数据的函数:

        requests.get = requests.get(source_url, headers=headers)
        Copier après la connexion

        使用装饰器的一个优点是只需要应用一次,并且每次都会对函数计时:

        data = requests.get(0)
        Copier après la connexion

        Elapsed time: 0.5512 seconds

        虽然@timer 顺利完成了对目标函数的定时。但从某种意义上说,你又回到了原点,因为该装饰器 @timer 失去了前面定义的类 Timer 的灵活性或便利性。换句话说,我们需要将 Timer 类表现得像一个装饰器。

        现在我们似乎已经将装饰器用作应用于其他函数的函数,但其实不然,因为装饰器必须是可调用的。Python中有许多可调用的类型,可以通过在其类中定义特殊的.__call__()方法来使自己的对象可调用。以下函数和类的行为类似:

        def square(num):
            return num ** 2
        
        square(4)
        Copier après la connexion

        16

        class Squarer:
            def __call__(self, num):
                return num ** 2
        
        square = Squarer()
        square(4)
        Copier après la connexion

        16

        这里,square 是一个可调用的实例,可以对数字求平方,就像square()第一个示例中的函数一样。

        我们现在向现有Timer类添加装饰器功能,首先需要 import functools。

        # timer.py
        import functools
        # ...
        @dataclass
        class Timer:
            # The rest of the code is unchanged
            def __call__(self, func):
                """Support using Timer as a decorator"""
                @functools.wraps(func)
                def wrapper_timer(*args, **kwargs):
                    with self:
                        return func(*args, **kwargs)
                return wrapper_timer
        Copier après la connexion

        在之前定义的上下文管理器 Timer ,给我们带来了不少便利。而这里使用的装饰器,似乎更加方便。

        @Timer(text="Downloaded the tutorial in {:.2f} seconds")
        def download_data():
            source_url = &#39;https://cloud.tsinghua.edu.cn/d/e1ccfff39ad541908bae/files/?p=%2Fall_six_datasets.zip&dl=1&#39;
            headers = {&#39;User-Agent&#39;: &#39;Mozilla/5.0&#39;}
            res = requests.get(source_url, headers=headers) 
        
        download_data()
        # Python Timer Functions: Three Ways to Monitor Your Code
        Copier après la connexion

        [ ... ]
        Downloaded the tutorial in 0.72 seconds

        有一种更直接的方法可以将 Python 计时器变成装饰器。其实上下文管理器和装饰器之间的一些相似之处:它们通常都用于在执行某些给定代码之前和之后执行某些操作

        基于这些相似之处,在 python 标准库中定义了一个名为 ContextDecoratormixin 类,它可以简单地通过继承 ContextDecorator 来为上下文管理器类添加装饰器函数。

        from contextlib import ContextDecorator
        # ...
        @dataclass
        class Timer(ContextDecorator):
            # Implementation of Timer is unchanged
        Copier après la connexion

        当以这种方式使用 ContextDecorator 时,无需自己实现 .__call__(),因此我们可以大胆地将其从 Timer 类中删除。

        使用 Python 定时器装饰器

        接下来,再最后一次重改 download_data.py 示例,使用 Python 计时器作为装饰器:

        # download_data.py
        import requests
        from timer import Timer
        @Timer()
        def main():
            source_url = &#39;https://cloud.tsinghua.edu.cn/d/e1ccfff39ad541908bae/files/?p=%2Fall_six_datasets.zip&dl=1&#39;
            headers = {&#39;User-Agent&#39;: &#39;Mozilla/5.0&#39;}
            res = requests.get(source_url, headers=headers) 
            with open(&#39;dataset/datasets.zip&#39;, &#39;wb&#39;) as f:
                f.write(res.content)
        if __name__ == "__main__":
            main()
        Copier après la connexion

        我们与之前的写法进行比较,唯一的区别是第 3 行的 Timer 的导入和第 4 行的 @Timer() 的应用。使用装饰器的一个显着优势是它们通常很容易调用

        但是,装饰器仍然适用于整个函数。这意味着代码除了记录了下载数据所需的时间外,还考虑了保存数据所需的时间。运行脚本:

        $ python download_data.py
        # Python Timer Functions: Three Ways to Monitor Your Code
        Copier après la connexion

        [ ... ]
        Elapsed time: 0.69 seconds

        从上面打印出来的结果可以看到,代码记录了下载数据和保持数据一共所需的时间。

        当使用 Timer 作为装饰器时,会看到与使用上下文管理器类似的优势:

        • 省时省力: 只需要一行额外的代码即可为函数的执行计时。

        • 可读性: 当添加装饰器时,可以更清楚地注意到代码会对函数计时。

        • 一致性: 只需要在定义函数时添加装饰器即可。每次调用时,代码都会始终如一地计时。

        然而,装饰器不如上下文管理器灵活,只能将它们应用于完整函数。

        Python 计时器代码

        这里展开下面的代码块以查看 Python 计时器timer.py的完整源代码。

        # timer.py
        import time
        from contextlib import ContextDecorator
        from dataclasses import dataclass, field
        from typing import Any, Callable, ClassVar, Dict, Optional
        
        class TimerError(Exception):
            """A custom exception used to report errors in use of Timer class"""
        
        @dataclass
        class Timer(ContextDecorator):
            """Time your code using a class, context manager, or decorator"""
        
            timers: ClassVar[Dict[str, float]] = {}
            name: Optional[str] = None
            text: str = "Elapsed time: {:0.4f} seconds"
            logger: Optional[Callable[[str], None]] = print
            _start_time: Optional[float] = field(default=None, init=False, repr=False)
        
            def __post_init__(self) -> None:
                """Initialization: add timer to dict of timers"""
                if self.name:
                    self.timers.setdefault(self.name, 0)
        
            def start(self) -> None:
                """Start a new timer"""
                if self._start_time is not None:
                    raise TimerError(f"Timer is running. Use .stop() to stop it")
        
                self._start_time = time.perf_counter()
        
            def stop(self) -> float:
                """Stop the timer, and report the elapsed time"""
                if self._start_time is None:
                    raise TimerError(f"Timer is not running. Use .start() to start it")
        
                # Calculate elapsed time
                elapsed_time = time.perf_counter() - self._start_time
                self._start_time = None
        
                # Report elapsed time
                if self.logger:
                    self.logger(self.text.format(elapsed_time))
                if self.name:
                    self.timers[self.name] += elapsed_time
        
                return elapsed_time
        
            def __enter__(self) -> "Timer":
                """Start a new timer as a context manager"""
                self.start()
                return self
        
            def __exit__(self, *exc_info: Any) -> None:
                """Stop the context manager timer"""
                self.stop()
        Copier après la connexion

        可以自己使用代码,方法是将其保存到一个名为的文件中timer.py并将其导入:

        from timer import Timer
        Copier après la connexion

        PyPI 上也提供了 Timer,因此更简单的选择是使用 pip 安装它:

        pip install codetiming
        Copier après la connexion

        注意,PyPI 上的包名称是codetiming,安装包和导入时都需要使用此名称Timer

        from codetiming import Timer
        Copier après la connexion

        除了名称和一些附加功能之外,codetiming.Timer timer.Timer 完全一样。总而言之,可以通过三种不同的方式使用 Timer

        1. 作为一个

        t = Timer(name="class")
        t.start()
        # Do something
        t.stop()
        Copier après la connexion

        2. 作为上下文管理器

        with Timer(name="context manager"):
            # Do something
        Copier après la connexion

        3. 作为装饰器

        @Timer(name="decorator")
        def stuff():
            # Do something
        Copier après la connexion

        这种 Python 计时器主要用于监控代码在单个关键代码块或函数上所花费的时间。

        其他 Python 定时器函数

        使用 Python 对代码进行计时有很多选择。这里我们学习了如何创建一个灵活方便的类,可以通过多种不同的方式使用该类。对 PyPI 的快速搜索发现,已经有许多项目提供 Python 计时器解决方案。

        在本节中,我们首先了解有关标准库中用于测量时间的不同函数的更多信息,包括为什么 perf_counter() 更好,然后探索优化代码的替代方案。

        使用替代 Python 计时器函数

        在本文之前,包括前面介绍python定时器的文章中,我们一直在使用 perf_counter() 来进行实际的时间测量,但是 Python 的时间库附带了几个其他也可以测量时间的函数。这里有一些:

        • time()

        • perf_counter_ns()

        • monotonic()

        • process_time()

        拥有多个函数的一个原因是 Python 将时间表示为浮点数。浮点数本质上是不准确的。之前可能已经看到过这样的结果:

        >>> 0.1 + 0.1 + 0.1
        0.30000000000000004
        
        >>> 0.1 + 0.1 + 0.1 == 0.3
        False
        Copier après la connexion

        Python 的 Float 遵循 IEEE 754 浮点算术标准,该标准以 64 位表示所有浮点数。因为浮点数有无限多位数,即不能用有限的位数来表达它们。

        考虑time()这个函数的主要目的,是它表示的是现在的实际时间。它以自给定时间点(称为纪元)以来的秒数来表示函数。time()返回的数字很大,这意味着可用的数字较少,因而分辨率会受到影响。简而言之, time()无法测量纳秒级差异:

        >>> import time
        >>> t = time.time()
        >>> t
        1564342757.0654016
        
        >>> t + 1e-9
        1564342757.0654016
        
        >>> t == t + 1e-9
        True
        Copier après la connexion

        一纳秒是十亿分之一秒。上面代码中,将纳秒添加到参数 t ,他并不会影响结果。与 time() 不同的是,perf_counter() 使用一些未定义的时间点作为它的纪元,它可以使用更小的数字,从而获得更好的分辨率:

        >>> import time
        >>> p = time.perf_counter()
        >>> p
        11370.015653846
        
        >>> p + 1e-9
        11370.015653847
        
        >>> p == p + 1e-9
        False
        Copier après la connexion

        众所周知,将时间表示为浮点数是非常具有挑战的一件事,因此 Python 3.7 引入了一个新选项:每个时间测量函数现在都有一个相应的 _ns 函数,它以 int 形式返回纳秒数,而不是以浮点数形式返回秒数。例如,time() 现在有一个名为 time_ns() 的纳秒对应项:

        import time
        time.time_ns()
        Copier après la connexion

        1564342792866601283

        整数在 Python 中是无界的,因此 time_ns() 可以为所有永恒提供纳秒级分辨率。同样,perf_counter_ns() perf_counter() 的纳秒版本:

        >>> import time
        >>> time.perf_counter()
        13580.153084446
        
        >>> time.perf_counter_ns()
        13580765666638
        Copier après la connexion

        我们注意到,因为 perf_counter() 已经提供纳秒级分辨率,所以使用 perf_counter_ns() 的优势较少。

        注意: perf_counter_ns() 仅在 Python 3.7 及更高版本中可用。在 Timer 类中使用了 perf_counter()。这样,也可以在较旧的 Python 版本上使用 Timer。

        有两个函数time不测量time.sleep时间:process_time()thread_time()。通常希望Timer能够测量代码所花费的全部时间,因此这两个函数并不常用。而函数 monotonic(),顾名思义,它是一个单调计时器,一个永远不会向后移动的 Python 计时器。

        除了 time() 之外,所有这些函数都是单调的,如果调整了系统时间,它也随之倒退。在某些系统上,monotonic()perf_counter() 的功能相同,可以互换使用。我们可以使用 time.get_clock_info() 获取有关 Python 计时器函数的更多信息:

        >>> import time
        >>> time.get_clock_info("monotonic")
        namespace(adjustable=False, implementation=&#39;clock_gettime(CLOCK_MONOTONIC)&#39;,
                  monotonic=True, resolution=1e-09)
        
        >>> time.get_clock_info("perf_counter")
        namespace(adjustable=False, implementation=&#39;clock_gettime(CLOCK_MONOTONIC)&#39;,
                  monotonic=True, resolution=1e-09)
        Copier après la connexion

        注意,不同系统上的结果可能会有所不同。

        PEP 418 描述了引入这些功能的一些基本原理。它包括以下简短描述:

        • time.monotonic(): 超时和调度,不受系统时钟更新影响

        • time.perf_counter():基准测试,短期内最精确的时钟

        • time.process_time():分析进程的CPU时间

        估计运行时间timeit

        在实际工作中,通常会想优化代码进一步提升代码性能,例如想知道将列表转换为集合的最有效方法。下面我们使用函数 set() 和直接花括号定义集合 {...} 进行比较,看看这两种方法哪个性能更优,此时需要使用 Python 计时器来比较两者的运行速度。

        >>> from timer import Timer
        >>> numbers = [7, 6, 1, 4, 1, 8, 0, 6]
        >>> with Timer(text="{:.8f}"):
        ...     set(numbers)
        ...
        {0, 1, 4, 6, 7, 8}
        0.00007373
        
        >>> with Timer(text="{:.8f}"):
        ...     {*numbers}
        ...
        {0, 1, 4, 6, 7, 8}
        0.00006204
        Copier après la connexion

        该测试结果表明直接花括号定义集合可能会稍微快一些,但其实这些结果非常不确定。如果重新运行代码,可能会得到截然不同的结果。因为这会受计算机的性能和计算机运行状态所影响:例如当计算机忙于其他任务时,就会影响我们程序的结果。

        更好的方法是多次重复运行相同过程,并获取平均耗时,就能够更加精确地测量目标程序的性能大小。因此可以使用 timeit 标准库,它旨在精确测量小代码片段的执行时间。虽然可以从 Python 导入和调用 timeit.timeit() 作为常规函数,但使用命令行界面通常更方便。可以按如下方式对这两种变体进行计时:

        $ python -m timeit --setup "nums = [7, 6, 1, 4, 1, 8, 0, 6]" "set(nums)"
        2000000 loops, best of 5: 163 nsec per loop
        
        $ python -m timeit --setup "nums = [7, 6, 1, 4, 1, 8, 0, 6]" "{*nums}"
        2000000 loops, best of 5: 121 nsec per loop
        Copier après la connexion

        timeit 自动多次调用代码以平均噪声测量。timeit 的结果证实 {*nums} 量比 set(nums) 快。

        注意:在下载文件或访问数据库的代码上使用 timeit 时要小心。由于 timeit 会自动多次调用程序,因此可能会无意中向服务器发送请求!

        最后,IPython 交互式 shell Jupyter Notebook 使用 %timeit 魔术命令对此功能提供了额外支持:

        In [1]: numbers = [7, 6, 1, 4, 1, 8, 0, 6]
        
        In [2]: %timeit set(numbers)
        171 ns ± 0.748 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)
        
        In [3]: %timeit {*numbers}
        147 ns ± 2.62 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)
        Copier après la connexion

        同样,测量结果表明直接花括号定义集合更快。在 Jupyter Notebooks 中,还可以使用 %%timeit cell-magic 来测量运行整个单元格的时间。

        使用 Profiler 查找代码中的Bottlenecks

        timeit 非常适合对特定代码片段进行基准测试。但使用它来检查程序的所有部分并找出哪些部分花费的时间最多会非常麻烦。此时我们想到可以使用分析器

        cProfile 是一个分析器,可以随时从标准库中访问它。可以通过多种方式使用它,尽管将其用作命令行工具通常是最直接的:

        $ python -m cProfile -o download_data.prof download_data.py
        Copier après la connexion

        此命令在打开分析器的情况下运行 download_data.py。将 cProfile 的输出保存在 download_data.prof 中,由 -o 选项指定。输出数据是二进制格式,需要专门的程序才能理解。同样,Python 在标准库中有一个选项 pstats!它可以在 .prof 文件上运行 pstats 模块会打开一个交互式配置文件统计浏览器。

        $ python -m pstats download_data.prof
        Welcome to the profile statistics browser.
        download_data.prof% help
        
        ...
        Copier après la connexion

        要使用 pstats,请在提示符下键入命令。通常你会使用 sortstats 命令,strip 可以获得更清晰的输出:

        download_data.prof% strip
        download_data.prof% sort cumtime
        download_data.prof% stats 10
        ...
        Copier après la connexion

        此输出显示总运行时间为 0.586 秒。它还列出了代码花费最多时间的十个函数。这里按累积时间 ( cumtime) 排序,这意味着当给定函数调用另一个函数时,代码会计算时间。

        总时间 ( tottime) 列表示代码在函数中花费了多少时间,不包括在子函数中的时间。要查找代码花费最多时间的位置,需要发出另一个sort命令:

        download_data.prof% sort tottime
        download_data.prof% stats 10
        ...
        Copier après la connexion

        可以使用 pstats了解代码大部分时间花在哪里,然后尝试优化我们发现的任何瓶颈。还可以使用该工具更好地理解代码的结构。例如,被调用者和调用者命令将显示给定函数调用和调用的函数。

        还可以研究某些函数。通过使用短语 timer 过滤结果来检查 Timer 导致的开销:

        download_data.prof% stats timer
        ...
        Copier après la connexion

        完成调查后,使用 quit 离开 pstats 浏览器。

        如需更加深入了解更强大的配置文件数据接口,可以查看 KCacheGrind[8]。它使用自己的数据格式,也可以使用 pyprof2calltree 从 cProfile 转换数据:

        $ pyprof2calltree -k -i download_data.prof
        Copier après la connexion

        该命令将转换 download_data.prof 并打开 KCacheGrind 来分析数据。

        这里为代码计时的最后一个选项是 line_profiler。cProfile 可以告诉我们代码在哪些函数中花费的时间最多,但它不会深入显示该函数中的哪些行最慢,此时就需要 line_profiler

        注意:还可以分析代码的内存消耗。这超出了本教程的范围,如果你需要监控程序的内存消耗,可以查看 memory-profiler。

        行分析需要时间,并且会为我们的运行时增加相当多的开销。正常的工作流程是首先使用 cProfile 来确定要调查的函数,然后在这些函数上运行 line_profilerline_profiler 不是标准库的一部分,因此应该首先按照安装说明进行设置。

        在运行分析器之前,需要告诉它要分析哪些函数。可以通过在源代码中添加 @profile 装饰器来实现。例如,要分析 Timer.stop(),在 timer.py 中添加以下内容:

        @profile
        def stop(self) -> float:
            # 其余部分不变
        Copier après la connexion

        注意,不需要导入profile配置文件,它会在运行分析器时自动添加到全局命名空间中。不过,我们需要在完成分析后删除该行。否则,会抛出一个 NameError 异常。

        接下来,使用 kernprof 运行分析器,它是 line_profiler 包的一部分:

        $ kernprof -l download_data.py
        Copier après la connexion

        此命令自动将探查器数据保存在名为 download_data.py.lprof 的文件中。可以使用 line_profiler 查看这些结果:

        $ python -m line_profiler download_data.py.lprof
        Timer unit: 1e-06 s
        
        Total time: 1.6e-05 s
        File: /home/realpython/timer.py
        Function: stop at line 35
        
        # Hits Time PrHit %Time Line Contents
        =====================================
        ...
        Copier après la connexion

        首先,注意本报告中的时间单位是微秒(1e-06 s)。通常,最容易查看的数字是 %Time,它告诉我们代码在每一行的函数中花费的总时间的百分比。

        Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

    Étiquettes associées:
    source:yisu.com
    Déclaration de ce site Web
    Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn
    Tutoriels populaires
    Plus>
    Derniers téléchargements
    Plus>
    effets Web
    Code source du site Web
    Matériel du site Web
    Modèle frontal