Области видимости в Django. Замыкания

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

from django.shortcuts import redirect
...

def only_user_view(request):
    if not request.user.is_authenticated:
        # Если пользователь не авторизован - отправляем его на страницу логина.
        return redirect('/auth/login/')
    # Если пользователь авторизован — здесь выполняется полезный код функции. 

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

Для решения подобных задач в Python применяют декораторы (иначе его называют обёртка, англ. "wrapper").

Декоратор — это паттерн проектирования, предназначенный для расширения функциональности объектов без вмешательства в их код. Именно декораторы позволят добавить проверку авторизации к любым view-функциям.

Чтобы понять принцип работы декораторов — нужно разобраться с понятиями «область видимости» и «замыкание».

Глобальная и локальная область видимости

global_variable = 'Глобальная'


def some_func(passed_variable):
    local_variable = 'Локальная'
    return(f'{global_variable} '
           f'{local_variable} '
           f'{passed_variable}')

print(some_func('Параметр'))
# Будет напечатано: Глобальная Локальная Параметр 

При попытке вызвать local_variable из глобальной области видимости появится сообщение об ошибке:

NameError: name 'local_variable' is not defined 

Вызываемая переменная определена в локальной области видимости some_func() и будет удалена сборщиком мусора после того, как функция завершит работу.

Функция some_func() возвращает строку, сформированную из значений трёх переменных.

  • Переменная global_variable объявлена вне этой функции, в глобальной области видимости (англ. global scope).
  • Переменная passed_variable передана в функцию в качестве параметра.
  • Переменная local_variable объявлена внутри функции, в локальной области видимости (англ. local scope).

Все три переменные доступны внутри функции (даже global_variable, которая не имеет к функции никакого отношения).

Функция может использовать переменную, объявленную во внешней области видимости, даже если эта переменная явно не передана в функцию. Обратиться к локальной переменной из глобальной области видимости не получится.

Вложенные функции

Объявим функцию внутри функции и вызовем эту вложенную функцию внутри объемлющей.

global_variable = 'Глобальная'


def some_func(passed_variable):
    local_variable = 'Локальная'

    def inside_func():
        inside_local_variable = 'Внутренняя'
        return(f'{global_variable} '
               f'{local_variable} '
               f'{passed_variable} '
               f'{inside_local_variable}')
    print(inside_func())

some_func('Параметр')
# Будет намечатано: Глобальная Локальная Параметр Внутренняя 

При попытке вызвать inside_func() из глобальной области появится сообщение об ошибке:

NameError: name 'inside_func' is not defined.  

Функция inside_func() доступна только в области видимости some_func().Переменная local_variable объявлена внутри функции some_func(), но вне вложенной функции inside_func(). Переменные, объявленные в объемлющей функции, для вложенной функции находятся в области видимости enclosing scope («объемлющая» или «контекстная» область видимости).У вложенных функций есть доступ к переменным в enclosing scope (независимо от уровня вложенности) и к глобальным переменным. Вызвать вложенную функцию можно только из её области видимости.

Функция возвращает функцию

Вложенную функцию inside_func() можно не только вызывать внутри some_func() (как это было в примере выше), но и вернуть её, как результат выполнения объемлющей функции. Возвращаемое значение можно присвоить переменной:

global_variable = 'Глобальная'


def some_func(passed_variable):
    local_variable = 'Локальная'

    def inside_func():
        inside_local_variable = 'Внутренняя'
        return (f'{global_variable} '
                f'{local_variable} '
                f'{passed_variable} '
                f'{inside_local_variable}')
    return inside_func

# Здесь вызывается функция some_func() 
# и результат её работы присваивается переменной kind_of_magic 
kind_of_magic = some_func('Параметр')

# Здесь рождается магия: some_func() вернула функцию, 
# значит, kind_of_magic — это функция
# и её можно вызвать:
print(kind_of_magic())
# Будет напечатано: Глобальная Локальная Параметр Внутренняя

# Можно создать ещё одну функцию
another_magic = some_func('Другой параметр')
print(another_magic())
# Будет напечатано: Глобальная Локальная Другой параметр Внутренняя 

kind_of_magic() и another_magic() — это функции в глобальной области видимости, которым доступны переменные из enclosing scope функции inside_func(). Каждая из этих функций — вполне самостоятельна и будет работать, даже если после создания этих функций уничтожить «родительскую» функцию some_func():

global_variable = 'Глобальная'


def some_func(passed_variable):
    local_variable = 'Локальная'

    def inside_func():
        inside_local_variable = 'Внутренняя'
        return(f'{global_variable} '
               f'{local_variable} '
               f'{passed_variable} '
               f'{inside_local_variable}')
    return inside_func

# Создали две функции
kind_of_magic = some_func('Параметр')
another_magic = some_func('Другой параметр')

del some_func  # Уничтожили функцию some_func()

# Всё равно сработает
print(kind_of_magic())
# Будет напечатано: Глобальная Локальная Параметр Внутренняя

print(another_magic())
# Будет напечатано: Глобальная Локальная Другой параметр Внутренняя

# А это уже не сработает: 
# some_func() удалена, создать новую функцию не получится.
misery_of_magic = some_func('Пробуем последний раз')
# NameError: name 'some_func' is not defined 

Функция some_func() — это, по факту, своеобразная фабрика для создания и настройки копий функции inside_func().После удаления some_func() вся её контекстная область видимости будет сохранена, потому что на эту область видимости существуют ссылки в объектах kind_of_magic и another_magic.

Вложенная функция принимает аргументы

Вложенная функция может принимать и обрабатывать аргументы.

def some_func(passed_variable):
    def inside_func(passed_inside_variable):
        return f'{passed_variable}, {passed_inside_variable}'
    return inside_func

# Создали функцию hello()
hello = some_func('Привет')
# В контексте функции hello() значением passed_variable будет строка 'Привет'

# Вызываем hello() с параметром для inside_func()
print(hello('Стёпа'))
# Будет напечатано: Привет, Стёпа

byebye = some_func('До свидания')
# В контексте функции byebye() 
# значением passed_variable будет строка 'До свидания'

print(byebye('Марк Лутц'))
# Будет напечатано: До свидания, Марк Лутц

# Но значение переменной passed_variable в контексте hello() сохранилось:
# оно по-прежнему 'Привет'
print(hello('Лера'))
# Будет напечатано: Привет, Лера 

Замыкание (closure) — это способность вложенной функции запоминать локальное состояние контекстной области объемлющей функции.

С помощью замыкания на основе some_func() были созданы функции hello() и byebye(), каждая со своей уникальной контекстной областью для inside_func().

ФУНКЦИЯЗНАЧЕНИЕ ПЕРЕМЕННОЙ PASSED_VARIABLE В КОНТЕКСТНОЙ ОБЛАСТИ ФУНКЦИИ INSIDE_FUNC()
hello()'Привет'
byebye()'До свидания'

У замыканий может быть и более сложное поведение:

# В зависимости от уровня сложности (volume)
# функция возвращает различное написание фразы.
def speech(text, volume):
    def whisper():
        return f'{text.lower()}...'

    def scream():
        return f'{text.upper()}!!!11'

    if volume < 50:
        return whisper
    return scream

easy_closure = speech('Замыкание - это просто', 99)
print(easy_closure())
# Будет напечатано: ЗАМЫКАНИЕ - ЭТО ПРОСТО!!!11 


Вы можете оставить комментарий, или Трекбэк с вашего сайта.

Оставить комментарий