I'm primarily a C++ developer, but I find myself writing significant amounts of Python these days. One C++ feature I miss in Python are function-scope static variables (variables which are initialised once, but retain their value across calls to the function). So I wrote a decorator for adding static variables to a function, and I'd like some feedback on it.
from functools import wraps
class Statics:
class Current:
class Value:
def __init__(self, name):
self.name = name
def __init__(self, statics):
self.__statics = statics
def __getattr__(self, name):
if hasattr(self.__statics, name):
return self.Value(name)
else:
return None
def __init__(self):
self.current = self.Current(self)
def __setattr__(self, name, value):
if isinstance(value, self.Current.Value):
assert value.name == name, \
f"static.current.{value.name} can only be use to assign to " \
f"static.{value.name}, not to static.{name}"
else:
super(Statics, self).__setattr__(name, value)
def with_statics(f):
"""Add static variables to a function.
A function decorated with @with_statics must accept a "static variables"
or "statics" object as its very first argument; the recommended name for
this parameter is 'static'. This "statics" object is used access the
function's static variables.
A static variable is initialised with a value the first time control flow
reaches its initialisation, and retains its value after that, even across
several calls to the function. To initialise a static variable, use the
following syntax: `static.x = static.current.x or expression`. When
executing this statement for the first time, `expression` will be
evaluated and stored in `static.x`. On all subsequent executions of this
statement (even on subsequent calls to the containing function), the
statement does nothing and `expression` is guaranteed to *not* be
evaluated.
Here's an example of using statics to implement a call counter:
>>> @with_statics
... def counter(static):
... static.val = static.current.val or 0
... val = static.val
... static.val += 1
... return val
>>> (counter(), counter(), counter())
(0, 1, 2)
The initialisation expression is guaranteed to only execute once:
>>> def get_string():
... print("Getting string")
... return ""
...
>>> @with_statics
... def record(static, text):
... static.recorded = static.current.recorded or get_string()
... static.recorded += text
... return static.recorded
...
>>> record("Hello")
Getting string
'Hello'
>>> record(", world!")
'Hello, world!'
Notice the absence of "Getting string" after the second call.
"""
statics = Statics()
@wraps(f)
def wrapper(*args, **kwargs):
return f(statics, *args, **kwargs)
return wrapper
Relevant parts of its test file:
import doctest
import unittest
import statics
from statics import *
class TestWithStatics(unittest.TestCase):
def test_simple(self):
@with_statics
def counter(static):
static.x = static.current.x or 0
static.x += 1
return static.x
self.assertSequenceEqual(
(counter(), counter(), counter()),
(1, 2, 3)
)
def test_unique_init_calls(self):
@with_statics
def counter(static, init):
static.x = static.current.x or init()
static.x += 1
return static.x
inits = []
def init():
inits.append(None)
return 0
self.assertSequenceEqual(
(counter(init), counter(init), counter(init)),
(1, 2, 3)
)
self.assertSequenceEqual(inits, [None])
def test_unique_init_loop(self):
@with_statics
def counter(static, init, count):
for i in range(count):
static.x = static.current.x or init()
static.x += 1
return static.x
inits = []
def init():
inits.append(None)
return 0
self.assertEqual(counter(init, 3), 3)
self.assertSequenceEqual(inits, [None])
@unittest.skipUnless(__debug__, "requires __debug__ run of Python")
def test_name_mismatch_assertion(self):
@with_statics
def function(static):
static.x = static.current.x or 0
static.y = static.current.x or 1
return static.y
with self.assertRaises(AssertionError) as ex:
function()
msg = str(ex.exception)
self.assertRegex(msg, r"static\.current\.x")
self.assertRegex(msg, r"static\.x")
self.assertRegex(msg, r"static\.y")
def test_instance_method(self):
class Subject:
def __init__(self):
self.val = 0
@with_statics
def act(static, self):
static.x = static.current.x or 0
self.val += static.x
static.x += 1
return self.val
s = Subject()
self.assertSequenceEqual(
(s.act(), s.act(), s.act()),
(0, 1, 3)
)
def test_class_method(self):
class Subject:
val = 0
@classmethod
@with_statics
def act(static, cls):
static.x = static.current.x or 0
cls.val += static.x
static.x += 1
return cls.val
self.assertSequenceEqual(
(Subject.act(), Subject.act(), Subject.act()),
(0, 1, 3)
)
def run():
if not doctest.testmod(statics)[0]:
print("doctest: OK")
unittest.main()
if __name__ == "__main__":
run()
I'm primarily looking for feedback in these areas:
- Is the implementation Pythonic? Being primarily a C++ developer, Python idioms don't come naturally to me; that's one thing I'm constantly trying to improve.
- Is the idea itself Pythonic (or at least neutral), or does it feel like something "true Python shouldn't have" for some reason?
- Am I reinventing a wheel and something like this already exists?
I'll welcome any other feedback too, of course.