Python: статические переменные в функциях

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

Что хотелось бы

Хотелось бы иметь аналог статической переменной в C/C++. Вот пример такой функции на С++:

1 int f()
2 {
3      static int cnt = 0;
4      return ++cnt;
5 }

Иногда нужно хранить состояния функции, которые не исчезают между вызовами.

Реализация на классе

Самый прямой и дубовый путь в питоне для этого, это использовать атрибуты классов. Т.е. хранить состояние в атрибуте класса, в __init__() его инициализировать, а в вызовах методов делать с этим атрибутом то, что нам нужно.

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

 1 class A():
 2 
 3     def __init__(self):
 4         self.cnt = 0
 5 
 6     def f(self):
 7         self.cnt += 1
 8         return self.cnt
 9 
10 a = A()
11 
12 print " Class A testing"
13 print a.f()
14 print a.f()
15 print a.f()
16 
17 > Class A testing
18 > 1
19 > 2
20 > 3

Реализация внутри функции

Предыдущий пример работает. Но есть и недостатки. Нужно определять класс, создавать его объект, вызвать метод объекта. А нельзя ли попроще, так, чтобы все было спрятано внутри функции и нам не нужно бы было заморачиваться с тем, что не относится собственно к нужному нам эффекту?

Можно!

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

Попробуем реализовать следующий план, например для того же счетчика вызовов:

  • Внутри функции создаем атрибут.
  • При первом вызове присваиваем ему начальное значение.
  • В следующих вызовах увеличиваем это значение на 1.

Код:

 1 def f():
 2     try:
 3         f.a += 1
 4     except AttributeError:
 5         f.a = 0
 6     return f.a
 7 
 8 print " Function f() testing"
 9 print f()
10 print f()
11 print f()
12 
13 > Function f() testing
14 > 1
15 > 2
16 > 3

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

Для чего это может понадобиться

Ну, один пример мы уже видели. Счетчик вызова функции.

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

Пусть мы берем эту информацию 100 раз для программы. Это не всегда может быть хорошо, 100 раз из сеанса дергать базу.

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

Но это не совсем удобно. Потому, например, что нужно обязательно помнить, что сначала нужно присвоить значение переменной, а потом уже к ней обращаться. В случае сложной логики это может быть нетривиальной задачей. А еще хуже то, что обращение к переменной может скрываться в какой нибудь редко вызываемой ветке алгоритма и если эта ветка не покрыта тестами. Да, бывает и такое в жизни, ваша программа вывалиться, когда наткнется на эту ветку.

А ведь можно сделать по нашему. Внутри функции, при первом ее вызове получить информацию из базы, запомнить ее в статический атрибут и при следующих вызовах возвращать уже этот атрибут.

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

 1 def get_session_id():
 2     try:
 3         return get_session_id.session_id
 4     except AttributeError:
 5         from myapp.mymodels import DbModel
 6         o = DbModel.objects.get(name="session_id")
 7         get_session_id.session_id = o.value
 8         get_session_id.session_id += 1
 9         o.value = get_session_id.session_id
10         o.save()
11         return get_session_id.session_id

Примечания к тексту функции:

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

Теперь вместо вызова кода, например:

1 session_id = get_session_id()
2 write_log(session_id,......)
3 write_log(session_id,......)
4 write_log(session_id,......)

Можем писать:

1 write_log(get_session_id(),......)
2 write_log(get_session_id(),......)
3 write_log(get_session_id(),......)

По моему неплохой метод.

Опубликовано: June 19, 2011

Комментарии:


Имя: Владимир

Неплохо, иногда бывает действительно удобно

Еще один вариант того же функционала, но уже с глобальным состоянием, иногда встречается:

cached_state = None
def function():
...if cached_state: return cached_state
...val = Model.objects.get(attr='value')
...cached_state = val
...return val

Еще один, с генератором:

def function():
...val = Model.objects.get(attr='value')
...yield val
...while True:
......yield val

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



Имя: evgeny

Не, ну глобальные переменные конечно это самый топорный метод, ИМХО, его даже не стоит рассмтривать. Я считаю, что области, для которых разрешены глобальные переменные, должны быть четко определены на уровне соглашений и политики фирмы. Считаю допустимым, например глобальные переменные для конфигурационных значений, общепрограммных флагов. В остальных случаях желательно избегать.



Имя: Alex

Учу питон, спасибо за инфу. хотелось бы еще про super() почитать.



Имя: Алеха

def test_getvar():
t = time.time()
sessid = get_id()
for i in range(0, 1000000):
id = sessid+i
return time.time()-t

def test_getfunc():
t = time.time()
sessid = get_id()
for i in range(0, 1000000):
id = get_id()+i
return time.time()-t

def test():
c = 0
for i in range(100): c+=test_getvar()
print('Среднее время с переменной:', c/100)
for i in range(100): c+=test_getfunc()
print('Среднее время с функцией:', c/100)

>>> test()
Среднее время с переменной: 0.17015625
Среднее время с функцией: 0.48390625



Имя: Алеха

def get_id():
return 123456



Комментировать:

Имя:

Комментарий: