"""
    Области видимости.

    1. built-in (встроенная в python) - переменные доступны в любом месте
        любой программы, они встроенные в python

    2. global (глобальная область видимости) - переменные, объявленные в
        программе и доступны в любом месте, после объявления

    3. local (локлаьная область видимости) - переменные, объявленные
        в теле функции

    4. nonlocal (enclosing - нелокальная или замкнутая область видимости)

"""

# -----------------------------------------------------------------------------
# Рассмотрим особенности
# global (глобальных переменных) и
# local (локальных переменных) при работе с функциями
# -----------------------------------------------------------------------------

a = 10  # это глобальная переменная


def func():
    # глобальные переменные доступны в теле функций, но только для чтения
    print(a)  # 10

    # a += 1
    # если попробовать изменить переменную, получим ошибку
    # UnboundLocalError: local variable 'a' referenced before assignment

    b = 20  # локальная переменная
    print(b)  # 20


func()
# print(b)  # NameError: name 'b' is not defined

a = 10
b = 20


def func():
    global a  # объявляем, что переменная a - глобальная
    a += 1  # теперь ее можно изменять
    print(a)  # 11

    b = 50  # локальная переменная
    print(b)  # 50


func()
print(a)  # 11
print(b)  # 20  (глобальная переменная после вызова функции не изменилась)

# -----------------------------------------------------------------------------
# Особенности nonlocal (нелокальных переменных) в замыканиях
# -----------------------------------------------------------------------------


def func():
    var = 10  # для функции func - это локальная переменная

    def inner():
        # в этой функции переменная "var" является нелокальной или замкнутой
        # она доступна только на чтение и чтобы изменять ее используем nonlocal
        # nonlocal var  # объявляем, что переменная "var" - нелокальная
        # var += 10
        print(var)  # 20

    return inner


inner_fun = func()
inner_fun()
inner_fun()


"""
    В python замыкания применяют только при использовании декораторов.
    декоратор == замыкание, но замыкание != декоратор.

    С использованием замыканий можно строить структуры подобные классам.
    Рассмотрим пример по аналогии с классом.
"""


def get_user_data(name, surname, age):
    # функция замыкает в себе name, surname, age
    def full_name():
        return f"{name.title()} {surname.title()}"

    def byear():
        return 2021 - age

    return {
        "name": name,  # вернем все переданные параметры ~ атрибуты класса
        "surname": surname,
        "age": age,
        "full_name": full_name(),  # вернем вызов функции ~ property в классах
        "byear": byear,  # вернем ссылку на функцию ~ метод класса
    }


# вызываем функцию и записываем результат в переменную
# ~ инициализируем класс и получаем объект (экземпляр) класса
user = get_user_data("Max", "Smith", 21)

# теперь у user по ключам доступны
# ~атрибуты
print(user["name"])
# ~свойства
print(user["full_name"])
# ~методы
print(user["byear"]())

# Лучше использовать классы!
