Замыкание и декораторы

С этими понятиями вы, скорее всего, не столкнетесь на первом кругу обучения языка программирования, а вероятнее всего и на втором. Мы разберемся с этими понятиями, не углубляясь в теоретическую часть, а на практике.

Замыкания

Замыкания представляю собой базовую конструкцию, но они являются сложными для понимания.

Давайте определим функцию, которая определяет объект, а у объекта есть поле, у поля есть значение.

>>> def fun(): pass

>>> fun
<function fun at 0x0396EF18>
>>> dir(fun)
['__annotations__', '__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__get__', '__getattribute__', '__globals__', '__gt__', '__hash__', '__init__', '__kwdefaults__', '__le__', '__lt__', '__module__', '__name__', '__ne__', '__new__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__']
>>> fun.__sizeof__
<built-in method __sizeof__ of function object at 0x0396EF18>
>>> fun.__sizeof__()
56
>>>

Есть отличие: наличие метода __call__, который говорит о том, что функцию можно вызвать.

Передадим функцию в качестве параметра другим функциям.

>>> callable(fun)
True
>>> fun()
>>> def ffun(t):
        return t(),t()

>>> ffun(fun)
(None, None)
>>>

Можно написать функцию, которая конструирует функцию.

>>> def constr():
        def fun(a):
                return a*2+1
        return fun

>>> constr()
<function constr.<locals>.fun at 0x039638E8>
>>>

При вызове constr возвращается fun. Обратим внимание на её родовое имя.

Модифицируем constr и передадим параметр.

>>> def constr(n):
        def fun(a):
                return a*2+n
        return fun

>>> g = constr(100500)
>>> g(2)
100504
>>>

Кажется, что здесь нет никакого подвоха. А так ли это?

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

Python анализирует функцию на содержание глобальных и локальных имен. Это делается для того, что бы случайно не перебить имя. И Python выясняет, что имя n приехало в пространство имен local, а потом происходит залипание. Т.е. появляется щель и n всегда будет доступно при вызове fun. Это и есть замыкание.

Посмотрим что интерпретируется у функции g.

>>> dir(g)
['__annotations__', '__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__get__', '__getattribute__', '__globals__', '__gt__', '__hash__', '__init__', '__kwdefaults__', '__le__', '__lt__', '__module__', '__name__', '__ne__', '__new__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__']
>>> dir(g.__closure__)
['__add__', '__class__', '__contains__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__getnewargs__', '__gt__', '__hash__', '__init__', '__iter__', '__le__', '__len__', '__lt__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__rmul__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'count', 'index']
>>> g.__closure__[0]
<cell at 0x036FFA90: int object at 0x036DA5A0>
>>> dir(a)
['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__le__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'cell_contents']
>>> a.cell_contents
100500
>>> type(g.__closure__)
<class 'tuple'>
>>> len(g.__closure__)
1
>>>

В tuple лежат не сами объекты, а ячейки. Покажем, что closure бывают развесистые.

>>> def f1(x):
        def f2():
                def f3():
                        return x
                return f3
        return f2

>>> f = f1(100500)
>>> f
<function f1.<locals>.f2 at 0x039639C0>
>>> dir(f)
['__annotations__', '__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__get__', '__getattribute__', '__globals__', '__gt__', '__hash__', '__init__', '__kwdefaults__', '__le__', '__lt__', '__module__', '__name__', '__ne__', '__new__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__']
>>> len(f.__closure__)
1
>>> f.__closure__[0].cell_contents
100500
>>> ff=f()
>>> ff
<function f1.<locals>.f2.<locals>.f3 at 0x03963A08>
>>>

В f1 есть локальное имя х, в f2 нет ничего, она создается в f3, а в f3 х используется и в итоге происходит залипание. Благодаря замыканию, мы можем создать конструктор функции, не думая, что происходит с пространством имен. По сути, мы приближаемся к функциональному программированию.

Вопрос: Обязательно ли имя локальное или глобальное?

Имя может быть не из этих пространств, а именно из nonlocal.

def f1():
    a = 42
    def f2():
        nonlocal a
        a+=1
    f2()
    f2()
    f2()
    return a

Хитрость:

def f1(x):
    def f2(a):
        return a+x
    return f2
>>> s = f1(2)
>>> s.__closure__
(<cell at 0x0363FA90: int object at 0x63C65700>,)
>>> s.__closure__[0].cell_contents
2
>>>

А если не такой эффект

def f1(x):
    def f2(a, t=x):
        return a+t
    return f2
>>> s=f1(2)
>>> s(2)
4
>>> s(5)
7
>>> len(s.__closure__)
Traceback (most recent call last):
  File "<pyshell#15>", line 1, in <module>
    len(s.__closure__)
TypeError: object of type 'NoneType' has no len()
>>>

Функциональность не изменилась. Этой функции не требуется замыкание. Этот объект теперь называется t.

Декораторы

Определим функцию, которая что-то делает

def fun(a,b,c):
    return a+b+c
z = fun(1,2,3)
print(z)

Хотим поправить fun так, что бы:

  1. С какими параметрами вызывали?
  2. Что собирается возвращать?

Теперь функция стала параметром

def fun(a,b,c):
    return a+b+c

def fwrap(t,*argp,**argn):
    print("WRAP",*argp,**argn)
    res = f(*argp,**arrgn)
    print("WRAP",res)
    return res
z = fwrap(fun,1,2,3)
print(z)

Пойдем более хитрым путем.

Создадим конструктор функции, который взял исходную функцию и вызвал её.

def fun(a,b,c):
    return a+b+c

def fwrap(t,*argp,**argn):
    print("WRAP",*argp,**argn)
    res = f(*argp,**arrgn)
    print("WRAP",res)
    return res

def fdec(f):
    def fun(*argp,**argn):
        print("WRAP",*argp,**argn)
        res = f(*argp,**argn)
        print("WRAP",res)
        return res
    return res

z = fwrap(fun,1,2,3)
print(z)

Напишем красивее

def fun(a,b,c):
    return a+b+c

def fwrap(t,*argp,**argn):
    print("WRAP",*argp,**argn)
    res = f(*argp,**arrgn)
    print("WRAP",res)
    return res

def fdec(f):
    def fun(*argp,**argn):
        print("WRAP",*argp,**argn)
        res = f(*argp,**argn)
        print("WRAP",res)
        return res
    return res

z = fwrap(fun,1,2,3)
y = fdec(fun)(1,2,3)
print(z,y)

Место, где происходит вызов fun, приходится преобразовывать.

#z = fwrap(fun,1,2,3)
#y = fdec(fun)(1,2,3)
fun = fdec(fun)
z = fun(1,2,3)
print(z)

Когда вызывается fun, всякий раз вызывается декодированная функция. Остается синтаксический сахар.

Хочется заранее знать, что fun обернута в декоратор. А здесь сначала определяется декоратор, потом fun, а потом производится обертывание.

В Python был принят синтаксис

#z = fwrap(fun,1,2,3)
#y = fdec(fun)(1,2,3)
#fun = fdec(fun)
z = fun(1,2,3)
y = fun(4,5,6)
print(z,y)

Сделаем конструктор декоратора

def deco(n):
    def dec2(f):
        def fun(*ap,**an):
            print(****n)
            return f(*ap,**an)
        return fun
    return dec2
@deco(4)
def fun(a):
    return a*2+1
z = fun(42)
print(z)

deco используется как декоратор вокруг fun.

Дескрипторы

class Desc:
    v = 0
    def __get__(self,o,c):
        return self.v
    def __set__(self,o,v):
        self.v = v
    def __delete__(self,o):
        pass

class C:
    p = Desc()

Вывод

>>> c = C()
>>> c.p
0
>>> c.p=9
>>> c.p
9
>>> dir(c)
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'p']
>>> c.__dict__
{}
>>>

Дескриптор не присваивает в dict, так как он является полем класса. Как определить self?

Есть 3 метода, а задекорирован только один. Значит, надо придумать еще 2 декоратора.

class Desc:
    v = 0
    def __get__(self,o,c):
        return self.v
    def __set__(self,o,v):
        self.v = v
    def __delete__(self,o):
        pass

class C:
    v = 0

    @property
    def x(self):
        return self.v

Допишем декораторы

class Desc:
    v = 0
    def __get__(self,o,c):
        return self.v
    def __set__(self,o,v):
        self.v = v
    def __delete__(self,o):
        pass

class C:
    p = Desc()
class D:
    v = 0

    @property
    def x(self):
        return self.v

    @x.setter
    def x(self,v):
        self.v = v

    @x.deleter
    def x(self):
        pass

LecturesCMC/PythonIntro2017/12_DecoratorClosure/Conspect (последним исправлял пользователь FrBrGeorge 2019-11-19 17:45:44)