Что такое декоратор? Почему именно функция внутри функции?

Огромная просьба, подскажите, пожалуйста, почему декоратор - это именно функция внутри функции? Почитал несколько статей, но нигде не вижу ответа конкретно на этот вопрос.

def func_decor2(some_func):
    print('Что-то делаем ДО')
    some_func()
    print('Что-то делаем после')

Чем не декоратор? Единственное, что приходит на ум по использованию дополнительной функции внутри - удобная передача аргументов, если наша some_func() будет требовать атрибуты. Огромная просьба внятно и чётко объяснить, как именно устроен декоратор и почему именно так, когда внутри декоратора обязательно должна быть еще одна функция-"обёртка". Заранее большое спасибо!


Ответы (3 шт):

Автор решения: vadim vaduxa

По большому счету декоратор это способ избежать многократного дублирования кода.

Предположим у вас есть код из 10 миллионов строк, и 100 однострочных функций которые вызываются в 1000 разных частях кода. И вдруг вам понадобилось добавить в эти функции какой-то функционал 'Что-то делаем ДО' и 'Что-то делаем после', который занимает 5000 срок.

Что вы будете делать, искать эти 1000 строк кода вызова функций, и для каждого вызова прописывать 'Что-то делаем ДО' и 'Что-то делаем после' 1000 раз, добавляя дополнительные 5 миллионов строк кода?

Или в каждую из 100 функций добавлять эти дополнительные 5000 строк кода, превращая 100 строк кода функций в 500 тысяч + 100 строк? А если потом вам надо будет отключить этот функционал для части функций, будете искать и удалять эти тысячи строк, сколько это займет времени, останется ли читабельным ваш код?

Или вы можете сделать декоратор, и вместо дополнительных 500 тысяч строк в вашем подходе, добавить всего 5 тысяч строк самого декоратора + 100 строк для декорирования функций. Более того, можно гибко управлять поведением декоратора, например через аргументы декоратора, либо вовсе отключить его, удалив лишь одну строчку кода @dec_arg() над функцией, либо вообще декорируя функцию по месту вызова, как ниже для func_2

DecEnable = True  # глобальная переменная, которая управляет поведением декоратора dec_arg, позволяет централизовано отключить дополнительный код всех декораторов, задекорированных без указания аргумента enable, т.е. как как @dec_arg()

def dec_arg(enable=DecEnable):
    def decor(func):  # тут 5000 строк дополнительного функционала, которые располагаются не внутри каждой из функций, а в одном месте, только здесь
        def wrap(*a, **k):
            if enable:
                print(func.__name__, f'Что-то делаем ДО {(a, k)}')
                try: return func(*a, **k)
                finally: print(func.__name__, f'Что-то делаем после {(a, k)}')
            else: return func(*a, **k)
        return wrap
    return decor

# ниже код из 10 миллионов строк, и 100 однострочных функций
@dec_arg(enable=False)  # поведение декоратора управляется из аргумента декоратора enable=False, в данном случае дополнительный код декоратора отключен
def func_1(a):
    print(a)

def func_2(a): # эта функция вообще не задекорирована
    print(a)

@dec_arg()  # () - без аргументов - поведение декоратора управляется глобальной переменной DecEnable, можно отключить дополнительный код всех декораторов, просто установив ее в False
def func_99(a):
    print(a)

@dec_arg()  # вместо 5000 дополнительных строк кода, мы добавили только одну, весь код находится в декораторе
def func_100(a):
    print(a)
# вызов функций в 1000 разных частях кода
func_1(1)  # вызов декорированной функции, но тут дополнительный код декоратора отключен в аргументе декорирования
dec_arg(enable=True)(func_2)(2)  # декорирование функции по месту ее вызова, независимо от состояния DecEnable, дополнительный код декоратора выполнится, т.к. enable=True
func_2(3)  # а тут вызов не задекорированной функции
func_99(4)  # вызов декорированной функции, поведение декоратора управляется глобальной переменной DecEnable
func_100(5)  # вызов декорированной функции, поведение декоратора управляется глобальной переменной DecEnable

Для этого и нужна функция обертка внутри декоратора, чтобы можно было декорировать множество функций одним и тем-же декоратором @dec_arg(), тем самым добавляя в них новую функциональность, но не добавляя внутри функций ни строчки лишнего кода

→ Ссылка
Автор решения: Stanislav Volodarskiy

То что вы написали есть декоратор, который декорирует это конкретный вызов функции. Чтобы его использовать в коде, надо менять код: если вы где-то вызывали оригинальную функцию теперь надо сменить её имя на декорированную.

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

Пример:

def f():
    print("I'm f!")


def g():
    f()
    f()

Чтобы декорировать f и воспользоваться новой версией надо написать:

def decorated(f):
    print("before f")
    f()
    print("after f")


def g2():
    decorated(f)
    decorated(f)


g2()

Пришлось заменить g на g2. Если мы собирались использовать декоратор для отладки, в этом мало смысла, надо переписать весь отлаживаемый код.

Вот вариант с обычным декоратором:

def decorated(f):

    def ff():
        print("before f")
        f()
        print("after f")

    return ff


f = decorated(f)
g()

Куда удобнее, g переписывать не надо, поведение f изменилось по всему коду, хотя сам код остался прежним.

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

→ Ссылка
Автор решения: SergFSM

попробую объяснить как я сам понимаю декораторы на примере вашего примера:

def func_decor2(some_func):
    print('Что-то делаем ДО')
    some_func()
    print('Что-то делаем после')

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

в случае же использования декоратора-обертки ничего переписывать не надо, пишите декоратор, обертываете им функцию sum_func() и теперь при каждом ее вызове будет срабатывать декоратор.

def func_decor2(some_func):
    def wrapper(*args,**kwargs):
        print('Что-то делаем ДО')
        some_func(*args,**kwargs)
        print('Что-то делаем после')
    return wrapper

@func_decor2()
def some_func():
    pass

для себя я понял так, что если у меня простой код и мне не сложно что-то добавить и поменять в работе функций, то и декораторы мне не нужны

→ Ссылка