Рубрики
Без рубрики

Расширенное использование декораторов Python

Этот учебник направлен на более интересное использование декораторов Python, в частности, на то, как декораторы могут использоваться в классах и как передавать дополнительные параметры функциям декоратора.

Автор оригинала: Sheena.

Вступление

Этот учебник основан на концепциях, представленных в этом учебнике . Если вы вообще не знакомы с декораторами или вас смущает какая-либо из тем, которые я здесь освещаю, я бы посоветовал вам пройти через упомянутый учебник и убедиться, что он имеет для вас смысл.

Этот учебник призван познакомить вас с некоторыми более интересными видами использования декораторов. В частности, как декораторы могут использоваться в классах и как передавать дополнительные параметры функциям декоратора.

Декораторы против Шаблон Декоратора

Шаблон декоратор – это объектно-ориентированный шаблон проектирования, который позволяет динамически добавлять поведение к существующему объекту. Когда вы украшаете объект, вы расширяете его функциональность таким образом, чтобы она не зависела от других экземпляров того же класса.

Декораторы Python не являются реализацией шаблона декоратора. Декораторы Python добавляют функциональность к функциям и методам во время определения, они не используются для добавления функциональности во время выполнения. Сам шаблон декоратора может быть реализован в Python, но это довольно тривиальная вещь из-за того, что Python типизирован уткой.

Базовый Декоратор

Это действительно простой пример того, что может сделать декоратор. Я включил его просто в качестве ориентира. Пожалуйста, убедитесь, что вы полностью понимаете этот код, прежде чем продолжить. Если вам нужно более подробное объяснение этого примера, пожалуйста, ознакомьтесь с основным учебником, прежде чем продолжить.

def time_this(original_function):      
    def new_function(*args,**kwargs):
        import datetime                 
        before = datetime.datetime.now()                     
        x = original_function(*args,**kwargs)                
        after = datetime.datetime.now()                      
        print "Elapsed Time = {0}".format(after-before)      
        return x                                             
    return new_function                                   
    
@time_this
def func_a(stuff):
    import time
    time.sleep(3)

func_a(1)

Декораторы, которые принимают Аргументы

Иногда декораторам полезно принимать аргументы помимо функции, которую они украшают. Этот вид техники часто используется для таких вещей, как регистрация функций. Известным примером этого является конфигурация представления в рамках веб-приложения Pyramid. Например:

@view_config(route_name='home', renderer='templates/mytemplate.pt')
def my_view(request):
    return {'project': 'hello decorators'}

Допустим, у нас есть приложение, в которое пользователи могут входить и взаимодействовать с хорошим графическим интерфейсом (графический интерфейс пользователя). Взаимодействие пользователя с графическим интерфейсом запускает события, которые вызывают выполнение функций Python. Давайте предположим, что есть много пользователей, использующих это приложение, и что у них есть куча разных уровней разрешений. Для выполнения различных функций требуются разные типы разрешений. Например, рассмотрим следующие функции:

#assume these functions exist
def current_user_id():
    """
    this function returns the current logged in user id, if the user is not authenticated then return None 
    """
    
def get_permissions(iUserId):
    """
    returns a list of permission strings for the given user. For example ['logged_in','administrator','premium_member']
    """

#we need to implment permission checking on these functions
    
def delete_user(iUserId):
   """
   delete the user with the given Id. This function is only accessable to users with administrator permissions
   """
   
def new_game():
    """
    any logged in user can start a new game
    """
    
def premium_checkpoint():
   """
   save the game progress, only accessable to premium members
   """

Одним из способов реализации этих разрешений было бы создание нескольких декораторов, например:

def requires_admin(fn):
    def ret_fn(*args,**kwargs):
        lPermissions = get_permissions(current_user_id())
        if 'administrator' in lPermissions:
            return fn(*args,**kwargs)
        else:
            raise Exception("Not allowed")
    return ret_fn

def requires_logged_in(fn):
    def ret_fn(*args,**kwargs):
        lPermissions = get_permissions(current_user_id())
        if 'logged_in' in lPermissions:
            return fn(*args,**kwargs)
        else:
            raise Exception("Not allowed")
    return ret_fn
    
def requires_premium_member(fn):
    def ret_fn(*args,**kwargs):
        lPermissions = get_permissions(current_user_id())
        if 'premium_member' in lPermissions:
            return fn(*args,**kwargs)
        else:
            raise Exception("Not allowed")
    return ret_fn
    
@requires_admin
def delete_user(iUserId):
   """
   delete the user with the given Id. This function is only accessable to users with administrator permissions
   """

@requires_logged_in 
def new_game():
    """
    any logged in user can start a new game
    """
    
@requires_premium_member
def premium_checkpoint():
   """
   save the game progress, only accessable to premium members
   """

Но это довольно ужасно. Это требует много копипаста, и каждый декоратор требует другого имени, и если какие-либо изменения вносятся в то, как проверяются разрешения, то каждый декоратор должен быть обновлен. Разве не было бы здорово иметь одного декоратора, который выполняет работу всех троих?

Для этого нам нужна функция, которая возвращает декоратор:

def requires_permission(sPermission):                            
    def decorator(fn):                                            
        def decorated(*args,**kwargs):                            
            lPermissions = get_permissions(current_user_id())     
            if sPermission in lPermissions:                       
                return fn(*args,**kwargs)                         
            raise Exception("permission denied")                  
        return decorated                                          
    return decorator       
    
    
def get_permissions(iUserId): #this is here so that the decorator doesn't throw NameErrors
    return ['logged_in',]

def current_user_id():        #ditto on the NameErrors
    return 1

#and now we can decorate stuff...                                     

@requires_permission('administrator')
def delete_user(iUserId):
   """
   delete the user with the given Id. This function is only accessible to users with administrator permissions
   """

@requires_permission('logged_in')
def new_game():
    """
    any logged in user can start a new game
    """
    
@requires_permission('premium_member')
def premium_checkpoint():
   """
   save the game progress, only accessable to premium members
   """

Попробуйте вызвать delete_user , new_game и premium_checkpoint и посмотрите, что произойдет.

И premium_checkpoint , и delete_user вызывают Исключение с сообщением “отказано в разрешении”, и new_game выполняется просто отлично (но мало что делает).

Вот общая форма декоратора с аргументами и иллюстрацией его использования:

def outer_decorator(*outer_args,**outer_kwargs):                            
    def decorator(fn):                                            
        def decorated(*args,**kwargs):                            
            do_something(*outer_args,**outer_kwargs)                      
            return fn(*args,**kwargs)                         
        return decorated                                          
    return decorator       
    
@outer_decorator(1,2,3)
def foo(a,b,c):
    print a
    print b
    print c


foo()

Это эквивалентно:

def decorator(fn):                                            
    def decorated(*args,**kwargs):                            
        do_something(1,2,3)                      
        return fn(*args,**kwargs)                         
    return decorated                                          
return decorator       
    
@decorator
def foo(a,b,c):
    print a
    print b
    print c


foo()

Классы декорирования

Декораторы не ограничиваются действием на функции, они также могут действовать на классы. Скажем, например, у нас есть класс, который делает много очень важных вещей, и мы хотим, чтобы время все, что он делает. Тогда мы могли бы использовать наш time_this декоратор из прошлого, например:

class ImportantStuff(object):
    @time_this
    def do_stuff_1(self):
        ...
    @time_this
    def do_stuff_2(self):
        ...
    @time_this
    def do_stuff_3(self):
        ...

Это сработало бы просто отлично. Но это довольно много дополнительных строк кода в классе. А что, если мы напишем еще несколько методов класса и забудем украсить один из них? Что, если мы решим, что больше не хотим назначать время для занятий? Здесь определенно есть место для человеческой ошибки. Было бы гораздо приятнее написать это так:

@time_all_class_methods
class ImportantStuff:
    def do_stuff_1(self):
        ...
    def do_stuff_2(self):
        ...
    def do_stuff_3(self):
        ...

Как вы уже знаете, этот код эквивалентен:

class ImportantStuff:
    def do_stuff_1(self):
        ...
    def do_stuff_2(self):
        ...
    def do_stuff_3(self):
        ...
        
ImportantStuff = time_all_class_methods(ImportantStuff)

Так как же будет работать time_all_class_methods ? Во-первых, мы знаем, что он должен принимать класс в качестве аргумента и возвращать класс. Мы также знаем, что функции возвращаемого класса должны выглядеть так же, как функции исходного класса ImportantStuff . То есть мы все еще хотим иметь возможность делать наши важные вещи, мы просто хотим, чтобы время было таким же. И вот как мы это сделаем:

def time_this(original_function):      
    print "decorating"                      
    def new_function(*args,**kwargs):
        print "starting timer"       
        import datetime                 
        before = datetime.datetime.now()                     
        x = original_function(*args,**kwargs)                
        after = datetime.datetime.now()                      
        print "Elapsed Time = {0}".format(after-before)      
        return x                                             
    return new_function  

def time_all_class_methods(Cls):
    class NewCls(object):
        def __init__(self,*args,**kwargs):
            self.oInstance = Cls(*args,**kwargs)
        def __getattribute__(self,s):
            """
            this is called whenever any attribute of a NewCls object is accessed. This function first tries to 
            get the attribute off NewCls. If it fails then it tries to fetch the attribute from self.oInstance (an
            instance of the decorated class). If it manages to fetch the attribute from self.oInstance, and 
            the attribute is an instance method then `time_this` is applied.
            """
            try:    
                x = super(NewCls,self).__getattribute__(s)
            except AttributeError:      
                pass
            else:
                return x
            x = self.oInstance.__getattribute__(s)
            if type(x) == type(self.__init__): # it is an instance method
                return time_this(x)                 # this is equivalent of just decorating the method with time_this
            else:
                return x
    return NewCls

#now lets make a dummy class to test it out on:

@time_all_class_methods
class Foo(object):
    def a(self):
        print "entering a"
        import time
        time.sleep(3)
        print "exiting a"

oF = Foo()
oF.a()

Вывод

В этом уроке я показал вам несколько трюков с использованием декораторов Python – я показал вам, как передавать аргументы вашим декораторам и как украшать классы. Но это все еще только верхушка айсберга. Существует множество рецептов использования декораторов во всевозможных странных ситуациях. Вы даже можете украсить своих декораторов (но если вы когда-нибудь дойдете до этого момента, вероятно, было бы неплохо провести проверку на вменяемость). В Python также есть несколько встроенных декораторов, о которых стоит знать, например staticmethod и classmethod .

Куда идти дальше? Обычно нет необходимости делать что-то более сложное с декораторами, чем то, что я показал вам в этом уроке. Если вас интересует больше способов изменения функциональности класса, я бы предложил прочитать о наследовании и общих принципах проектирования OO. Или, если вы действительно хотите заставить их танцевать, тогда читайте о метаклассах (но опять же, иметь дело с этим материалом вряд ли когда-либо понадобится).