На этом шаге мы рассмотрим реализацию такого доступа.
Вы хотите добавить в замыкание функции, которые позволят получать доступ и изменять внутренние переменные.
В обычном случае внутренние переменные замыкания полностью скрыты от внешнего мира. Однако вы можете предоставить доступ путем написания функций для доступа и прикрепления их к замыканию в качестве атрибутов функции. Например:
>>> def sample(): n = 0 # Функция-замыкание def func(): print('n=', n) # Методы доступа к n def get_n(): return n def set_n(value): nonlocal n n = value # Прикрепление в качестве атрибутов функции func.get_n = get_n func.set_n = set_n return func >>>
Вот пример использования этого кода:
>>> f = sample() >>> f() n= 0 >>> f.set_n(10) >>> f() n= 10 >>> f.get_n() 10 >>>
Две главные возможности языка позволяют этому рецепту работать. Во-первых, инструкции nonlocal делают возможным написание функций, которые изменяют внутренние переменные. Во-вторых, атрибуты функции дают возможность напрямую прикреплять методы для доступа к замыканию, и они работают практически так же, как методы экземпляра (хотя классы тут не используются).
Небольшое дополнение к этому рецепту позволит замыканиям эмулировать экземпляры класса. Все, что вам нужно, - это скопировать внутренние функции в словарь экземпляра и возвратить его. Например:
>>> import sys >>> class ClosureInstance: def __init__(self, locals=None): if locals is None: locals = sys._getframe(1).f_locals # Обновить словарь экземпляра вызываемыми объектами self.__dict__.update((key,value) for key, value in locals.items() \ if callable(value)) # перегружаем специальные методы def __len__(self): return self.__dict__['__len__']() >>> # Пример использования >>> def Stack(): items = [] def push(item): items.append(item) def pop(): return items.pop() def __len__(): return len(items) return ClosureInstance() >>>
Вот интерактивный сеанс, который показывает, как все это работает:
>>> s = Stack() >>> s <__main__.ClosureInstance object at 0x0000017BE66F8880> >>> s.push(10) >>> s.push(20) >>> s.push('Hello') >>> len(s) 3 >>> s.pop() 'Hello' >>> s.pop() 20 >>> s.pop() 10 >>>
Интересно, что этот код работает немного быстрее аналога, использующего обычное определение класса. Например, вы можете проверить производительность по сравнению с таким классом:
>>> class Stack2: def __init__(self): self.items = [] def push(self, item): self.items.append(item) def pop(self): return self.items.pop() def __len__(self): return len(self.items) >>>
Если вы это сделаете, то получите похожие результаты:
>>> from timeit import timeit >>> # Тест с использованием замыканий >>> s = Stack() >>> timeit('s.push(1);s.pop()', 'from __main__ import s') 1.1470074999999724 >>> # Тест с использованием класса >>> s = Stack2() >>> timeit('s.push(1);s.pop()', 'from __main__ import s') 1.2185194000001047 >>>
Как показано выше, версия на базе замыкания работает на 8% быстрее. По большей части выигрыш возникает за счет прямого доступа к переменным экземпляра. Замыкания быстрее, потому что не используют дополнительную переменную self. Однако если вы склоняетесь использовать что-то такое в своей программе, помните, что это просто диковинная замена настоящему классу. Например, ключевые возможности типа наследования, свойств, дескрипторов или методов класса работать не будут. Вам также придется поплясать с бубном, чтобы заставить специальные методы работать (например, обратите внимание на реализацию метода __len__() в ClosureInstance).
И последнее: вы рискуете затруднить жизнь людям, которые будут читать ваш код и размышлять о том, почему в нем нет нормального определения класса (конечно, они также задумаются, почему ваша версия работает быстрее). Но, в любом случае, это интересный пример того, чего можно достичь, предоставляя доступ к "внутренностям" замыкания.
Добавление методов в замыкания может иметь больше смысла в задачах, в рамках которых вам нужно делать вещи типа сброса внутреннего состояния, сброса буферов, очистки кеша или реализации какого-то механизма обратной связи.
Со следующего шага мы начнем рассматривать классы и объекты.