Шаг 128.
Python: сборник рецептов.
Функции. Доступ к переменным, определенным внутри замыкания

    На этом шаге мы рассмотрим реализацию такого доступа.

Задача

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

Решение

    В обычном случае внутренние переменные замыкания полностью скрыты от внешнего мира. Однако вы можете предоставить доступ путем написания функций для доступа и прикрепления их к замыканию в качестве атрибутов функции. Например:

>>> 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).

    И последнее: вы рискуете затруднить жизнь людям, которые будут читать ваш код и размышлять о том, почему в нем нет нормального определения класса (конечно, они также задумаются, почему ваша версия работает быстрее). Но, в любом случае, это интересный пример того, чего можно достичь, предоставляя доступ к "внутренностям" замыкания.

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

    Со следующего шага мы начнем рассматривать классы и объекты.




Предыдущий шаг Содержание Следующий шаг