Дальше у меня возник вопрос и ступор - мы почему то возвращаем не результат выполнения функции wrapper, а возвращаем саму функцию-объект.
Так и есть. Нам же не нужно в самом декораторе вызывать функцию, а только указать, какую функцию мы используем. Без скобок () это просто ссылка на сам объект (функцию).
Пример:
def f1(): return 1
a = f1() # присваиваем результат работы функции, т.е. в a будет 1
a = f1 # присваиваем саму функцию
# тогда a - это не результат работы функции f1, а сама функция f1
# и мы можем ее запустить
print(a() ) # выведет 1, т.к. мы через a запустили f1
Декораторы так и делают. Создают функцию и возвращают ее, но не запускают. А запускается эта "обертка" во время запуска нашей основной функции.
Т.е. когда добавляется декоратор
@uppercase
def greet(): ...
# то когда мы запускаем функцию
greet()
# по факту запускается
uppercase(greet)()
Но что возвращает uppercase(greet) ? Смотрим по коду. А возвращает она некий wrapper. Что делает wrapper?
original_result = func() # запускает переданную ему функцию на исполнение, в нашем случае greet
return f'Большие {original_result.upper()}' # и возвращает строку с результатами исполнения
И она сама каким-то образом вызывается и выполняется, хоть мы и явно не пишем это.
Исполняется потому, что запуск декорированной функции означает запуск
uppercase(greet)()
а т.к. uppercase(greet) возвращает wrapper
то uppercase(greet)() запускает wrapper()