Introduction détaillée à l'utilisation des décorateurs Python (exemples de code)

不言
Libérer: 2019-02-25 10:33:34
avant
2711 Les gens l'ont consulté

Ce que cet article vous apporte est une introduction détaillée à l'utilisation (exemple de code) des décorateurs Python. Il a une certaine valeur de référence. Les amis dans le besoin peuvent s'y référer.

En Python, les décorateurs sont généralement utilisés pour décorer des fonctions afin d'implémenter des fonctions publiques et réaliser la réutilisation du code. Ajoutez @xxxx avant la définition de la fonction, puis la fonction injectera certains comportements, ce qui est incroyable ! Cependant, ce n’est que du sucre syntaxique.

Scénario

Supposons que certaines fonctions de travail soient utilisées pour traiter les données de différentes manières :

def work_bar(data):
    pass


def work_foo(data):
    pass
Copier après la connexion

Nous voulons le faire avant d'appeler le fonction Que dois-je faire si le journal est affiché après /?

Solution insensée

logging.info('begin call work_bar')
work_bar(1)
logging.info('call work_bar done')
Copier après la connexion

Et s'il y a plusieurs appels de code ? Rien que d'y penser, ça me fait peur !

Enveloppement de fonctions

La solution idiote n'est rien de plus qu'une trop grande redondance de code, et vous devez la réécrire pour chaque appel de fonctionlogging. Vous pouvez encapsuler cette partie de logique redondante dans une nouvelle fonction :

def smart_work_bar(data):
    logging.info('begin call: work_bar')
    work_bar(data)
    logging.info('call doen: work_bar')
Copier après la connexion

De cette façon, appelez smart_work_bar à chaque fois :

smart_work_bar(1)

# ...

smart_work_bar(some_data)
Copier après la connexion

Fermeture générale

Cela a l'air parfait... Cependant, lorsque work_foo a également le même besoin, devons-nous le mettre en œuvre à nouveau smart_work_foo ? Ce n’est évidemment pas scientifique !

Ne vous inquiétez pas, nous pouvons utiliser des fermetures :

def log_call(func):
    def proxy(*args, **kwargs):
        logging.info('begin call: {name}'.format(name=func.func_name))
        result = func(*args, **kwargs)
        logging.info('call done: {name}'.format(name=func.func_name))
        return result
    return proxy
Copier après la connexion

Cette fonction reçoit un objet fonction (fonction proxy) en paramètre et renvoie une fonction proxy. Lors de l'appel de la fonction proxy, le journal est d'abord affiché, puis la fonction proxy est appelée, le journal est affiché une fois l'appel terminé et enfin le résultat de l'appel est renvoyé. De cette façon, n’atteint-il pas l’objectif de généralisation ? ——Pour toute fonction proxy func, log_call peut être facilement gérée.

smart_work_bar = log_call(work_bar)
smart_work_foo = log_call(work_foo)

smart_work_bar(1)
smart_work_foo(1)

# ...

smart_work_bar(some_data)
smart_work_foo(some_data)
Copier après la connexion

Dans la ligne 1, log_call reçoit le paramètre work_bar, renvoie une fonction proxy proxy et l'assigne à smart_work_bar. Dans la ligne 4, appelez smart_work_bar, qui est la fonction proxy proxy, affichez d'abord le journal, puis appelez func, qui est work_bar, et enfin affichez le journal. Notez que dans la fonction proxy, func est étroitement lié à l'objet work_bar transmis. Il s'agit de la fermeture .

Encore une fois, vous pouvez écraser le nom de la fonction proxy. Le préfixer avec smart_ pour obtenir un nouveau nom est encore un peu fastidieux :

work_bar = log_call(work_bar)
work_foo = log_call(work_foo)

work_bar(1)
work_foo(1)
Copier après la connexion

Sucre syntaxique

. vient en premier Jetez un oeil au code suivant :

def work_bar(data):
    pass
work_bar = log_call(work_bar)


def work_foo(data):
    pass
work_foo = log_call(work_foo)
Copier après la connexion

Bien que le code ne soit plus redondant, il n'est toujours pas assez intuitif. À ce stade, le sucre de syntaxe arrive ~~~

@log_call
def work_bar(data):
    pass
Copier après la connexion

Par conséquent, faites attention à une chose (c'est nous qui soulignons La fonction de @log_call ici est simplement de dire le compilateur pour insérer du code Python. work_bar = log_call(work_bar)

Évaluation Décorateur

Devinez d'abord ce que fait le décorateur

 ? eval_now

def eval_now(func):
    return func()
Copier après la connexion
Ça a l'air étrange. Il n'y a pas de fonction proxy définie. Est-il considéré comme un décorateur ?

@eval_now
def foo():
    return 1

print foo
Copier après la connexion
Ce code génère

, qui consiste à appeler et à évaluer la fonction. Alors à quoi ça sert ? Tu ne peux pas simplement écrire 1 directement ? Dans cet exemple simple, il est bien entendu possible d’écrire ainsi. Regardons un exemple plus complexe : initialisez un objet de journal : foo = 1

# some other code before...

# log format
formatter = logging.Formatter(
    '[%(asctime)s] %(process)5d %(levelname) 8s - %(message)s',
    '%Y-%m-%d %H:%M:%S',
)

# stdout handler
stdout_handler = logging.StreamHandler(sys.stdout)
stdout_handler.setFormatter(formatter)
stdout_handler.setLevel(logging.DEBUG)

# stderr handler
stderr_handler = logging.StreamHandler(sys.stderr)
stderr_handler.setFormatter(formatter)
stderr_handler.setLevel(logging.ERROR)

# logger object
logger = logging.Logger(__name__)
logger.setLevel(logging.DEBUG)
logger.addHandler(stdout_handler)
logger.addHandler(stderr_handler)

# again some other code after...
Copier après la connexion
Utilisez la méthode

 : eval_now

# some other code before...

@eval_now
def logger():
    # log format
    formatter = logging.Formatter(
        '[%(asctime)s] %(process)5d %(levelname) 8s - %(message)s',
        '%Y-%m-%d %H:%M:%S',
    )

    # stdout handler
    stdout_handler = logging.StreamHandler(sys.stdout)
    stdout_handler.setFormatter(formatter)
    stdout_handler.setLevel(logging.DEBUG)

    # stderr handler
    stderr_handler = logging.StreamHandler(sys.stderr)
    stderr_handler.setFormatter(formatter)
    stderr_handler.setLevel(logging.ERROR)

    # logger object
    logger = logging.Logger(__name__)
    logger.setLevel(logging.DEBUG)
    logger.addHandler(stdout_handler)
    logger.addHandler(stderr_handler)

    return logger

# again some other code after...
Copier après la connexion
Le but des deux morceaux de code est le même. Mais ce dernier est évidemment plus clair et a le style de blocs de code. Plus important encore, les appels de fonction sont initialisés dans l'espace de noms local pour éviter que les variables temporaires (telles que

, etc.) ne polluent les espaces de noms externes (tels que le global). formatter

Décorateur avec paramètres

Définir un décorateur pour enregistrer les appels de fonction lents :

def log_slow_call(func):
    def proxy(*args, **kwargs):
        start_ts = time.time()
        result = func(*args, **kwargs)
        end_ts = time.time()

        seconds = start_ts - end_ts
        if seconds > 1:
        logging.warn('slow call: {name} in {seconds}s'.format(
            name=func.func_name,
            seconds=seconds,
        ))

        return result

    return proxy
Copier après la connexion
Les lignes

et 3 sont respectivement dans les appels de fonction Echantillonnage de l'heure actuelle avant et après, la ligne 5 calcule la durée de l'appel et génère un journal d'avertissement si cela prend plus d'une seconde. 7

@log_slow_call
def sleep_seconds(seconds):
    time.sleep(seconds)

sleep_seconds(0.1)  # 没有日志输出

sleep_seconds(2)    # 输出警告日志
Copier après la connexion
Cependant, le réglage du seuil dépend toujours de la situation et différentes fonctions peuvent définir des valeurs différentes. Ce serait bien s'il y avait un moyen de paramétrer le seuil :

def log_slow_call(func, threshold=1):
    def proxy(*args, **kwargs):
        start_ts = time.time()
        result = func(*args, **kwargs)
        end_ts = time.time()

        seconds = start_ts - end_ts
        if seconds > threshold:
        logging.warn('slow call: {name} in {seconds}s'.format(
            name=func.func_name,
            seconds=seconds,
        ))

        return result

    return proxy
Copier après la connexion
Cependant, le sucre syntaxique

appelle toujours le décorateur avec la fonction décorée comme paramètre, ce qui signifie qu'il n'y a aucune chance de passez le paramètre @xxxx. Ce qu'il faut faire? ——Utilisez une fermeture pour encapsuler les paramètres threshold : threshold

def log_slow_call(threshold=1):
    def decorator(func):
        def proxy(*args, **kwargs):
            start_ts = time.time()
            result = func(*args, **kwargs)
            end_ts = time.time()

            seconds = start_ts - end_ts
            if seconds > threshold:
            logging.warn('slow call: {name} in {seconds}s'.format(
                name=func.func_name,
                seconds=seconds,
            ))

            return result

        return proxy

    return decorator


@log_slow_call(threshold=0.5)
def sleep_seconds(seconds):
    time.sleep(seconds)
Copier après la connexion
De cette façon,

appelle la fonction de retour log_slow_call(threshold=0.5), et la fonction a une variable de fermeture decorator avec une valeur de threshold. 0.5Décorezdecorator à nouveau. sleep_seconds

En utilisant le seuil par défaut, l'appel de fonction ne peut pas être omis :

@log_slow_call()
def sleep_seconds(seconds):
    time.sleep(seconds)
Copier après la connexion
La Vierge peut être mécontente de la paire de parenthèses dans la première ligne, elle peut donc être améliorée comme ceci :

def log_slow_call(func=None, threshold=1):
    def decorator(func):
        def proxy(*args, **kwargs):
            start_ts = time.time()
            result = func(*args, **kwargs)
            end_ts = time.time()

            seconds = start_ts - end_ts
            if seconds > threshold:
            logging.warn('slow call: {name} in {seconds}s'.format(
                name=func.func_name,
                seconds=seconds,
            ))

            return result

        return proxy

    if func is None:
        return decorator
    else:
        return decorator(func)
Copier après la connexion
Cette méthode d'écriture est compatible avec deux usages différents, l'usage

seuil par défaut (pas d'usage d'appel A seuil personnalisé (avec appel). Dans l'utilisation B

# Case A
@log_slow_call
def sleep_seconds(seconds):
    time.sleep(seconds)


# Case B
@log_slow_call(threshold=0.5)
def sleep_seconds(seconds):
    time.sleep(seconds)
Copier après la connexion

, ce qui se passe est A, c'est-à-dire que le paramètre log_slow_call(sleep_seconds) n'est pas vide. Ceci est directement appelé func pour envelopper et renvoyer (le seuil est. la valeur par défaut). decorator

用法B中,先发生的是log_slow_call(threshold=0.5)func参数为空,直接返回新的装饰器decorator,关联闭包变量threshold,值为0.5;然后,decorator再装饰函数sleep_seconds,即decorator(sleep_seconds)。注意到,此时threshold关联的值是0.5,完成定制化。

你可能注意到了,这里最好使用关键字参数这种调用方式——使用位置参数会很丑陋:

# Case B-
@log_slow_call(None, 0.5)
def sleep_seconds(seconds):
    time.sleep(seconds)
Copier après la connexion

当然了,函数调用尽量使用关键字参数是一种极佳实践,含义清晰,在参数很多的情况下更是如此。

智能装饰器

上节介绍的写法,嵌套层次较多,如果每个类似的装饰器都用这种方法实现,还是比较费劲的(脑子不够用),也比较容易出错。

假设有一个智能装饰器smart_decorator,修饰装饰器log_slow_call,便可获得同样的能力。这样,log_slow_call定义将变得更清晰,实现起来也更省力啦:

@smart_decorator
def log_slow_call(func, threshold=1):
    def proxy(*args, **kwargs):
        start_ts = time.time()
        result = func(*args, **kwargs)
        end_ts = time.time()

        seconds = start_ts - end_ts
        if seconds > threshold:
        logging.warn('slow call: {name} in {seconds}s'.format(
            name=func.func_name,
            seconds=seconds,
        ))

        return result

    return proxy
Copier après la connexion

脑洞开完,smart_decorator如何实现呢?其实也简单:

def smart_decorator(decorator):

    def decorator_proxy(func=None, **kwargs):
        if func is not None:
            return decorator(func=func, **kwargs)

        def decorator_proxy(func):
            return decorator(func=func, **kwargs)

        return decorator_proxy

    return decorator_proxy
Copier après la connexion

smart_decorator实现了以后,设想就成立了!这时,log_slow_call,就是decorator_proxy(外层),关联的闭包变量decorator是本节最开始定义的log_slow_call(为了避免歧义,称为real_log_slow_call)。log_slow_call支持以下各种用法:

# Case A
@log_slow_call
def sleep_seconds(seconds):
    time.sleep(seconds)
Copier après la connexion

用法A中,执行的是decorator_proxy(sleep_seconds)(外层),func非空,kwargs为空;直接执行decorator(func=func, **kwargs),即real_log_slow_call(sleep_seconds),结果是关联默认参数的proxy

# Case B
# Same to Case A
@log_slow_call()
def sleep_seconds(seconds):
    time.sleep(seconds)
Copier après la connexion

用法B中,先执行decorator_proxy()funckwargs均为空,返回decorator_proxy对象(内层);再执行decorator_proxy(sleep_seconds)(内层);最后执行decorator(func, **kwargs),等价于real_log_slow_call(sleep_seconds),效果与用法A一致。

# Case C
@log_slow_call(threshold=0.5)
def sleep_seconds(seconds):
    time.sleep(seconds)
Copier après la connexion

用法C中,先执行decorator_proxy(threshold=0.5)func为空但kwargs非空,返回decorator_proxy对象(内层);再执行decorator_proxy(sleep_seconds)(内层);最后执行decorator(sleep_seconds, **kwargs),等价于real_log_slow_call(sleep_seconds, threshold=0.5),阈值实现自定义!

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:segmentfault.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