2

It is well established that Python does not have "static class variables" in the same sense as in other languages. Instead, it has class attributes, which are similar, but very different.

For example, a class attribute (a variable defined inside of a class definition) can be "overridden" by an instance attribute of the same name:

class A():
    i = 1

assert A.i == 1
a = A()
assert a.i == 1
a.i = 2 # has A.i been updated to 2 as well?
assert A.i == 2 # ERROR: no, it hasn't
assert a.i == 2 # A.i and a.i are two different variables
del a.i
assert a.i == 1 # If a.i doesn't exist, falls back in A.i

In actuality, there is no "overriding" going on - the class dictionary just comes later in the attribute lookup order than the instance dictionary.

This behavior can be partially overcome using the property decorator:

class A():
    _i = 1
    @property
    def i(self):
        return type(self)._i
    @i.setter
    def i(self,value):
        type(self)._i = value

Now, the property will remain "in sync" between the multiple instances:

a1 = A()
a2 = A()
a1.i = 2
assert a1.i == a2.i # i attribute remained in sync

However, the "gotcha" here is that you cannot access or set the property via the class itself:

assert A.i == a.i # ERROR
type(A.i) # class 'property'
A.i = 10 # if I try to set via the class...
assert a.i == 10 # it appears to have worked!
a.i = 5 # but...
assert A.i == a.i # ERROR! It didn't work. 
type(A.i) # int - setting the property via the class actually overwrote it

This difficulty can be overcome using a full-fledged descriptor (of which property is only one type):

class MyProperty():
    def __init__(self,name):
        self.name = name
    def __get__(self,inst,cls):
        return getattr(cls,self.name)
    def __set__(self,inst,value):
        setattr(type(inst),self.name,value)
    def __delete__(self,inst):
        delattr(type(inst),self.name)

class A():
    i = MyProperty('i')

Now, getting and setting the attribute via the class and via the instance all work, and deleting via the instance also works:

a = A()
A.i = 5 # Can set via the class
assert a.i == A.i
a.i = 10
assert a.i == A.i # Still in sync!
del a.i
assert A.i # Error, as expected
A.i = 2
assert a.i == 2 

However, there is still a problem when you try to delete via the class:

del A.i # The MyProperty descriptor has been deleted
A.i = 2
a.i = 4
assert A.i == a.i # ERROR!

How can full emulation of "static variables" be achieved in Python?

Community
  • 1
  • 1
Rick
  • 43,029
  • 15
  • 76
  • 119

2 Answers2

1

I present the (Python 3) solution below for informational purposes only. I am not endorsing it as a "good solution". I have my doubts as to whether emulating the static variable behavior of other languages in Python is ever actually necessary. However, regardless as to whether it is actually useful, creating the code below helped me further understand how Python works, so it might help others as well.

The metaclass I have created below attempts to emulate "static variable" behavior of other languages. It's pretty complicated, but it basically works by replacing the normal getter, setter, and deleter with versions which check to see if the attribute being requested is a "static variable". A catalog of the "static variables" is stored in the StaticVarMeta.statics attribute. If the requested attribute is not a "static variable", the class will fall back on the default attribute get/set/delete behavior. If it IS a "static variable", it attempted to resolve the attribute request using a substitute resolution order (which I have dubbed the __sro__, or "static resolution order").

I am sure there are simpler ways to accomplish this, and look forward to seeing other answers.

from functools import wraps

class StaticVarsMeta(type):
    '''A metaclass for creating classes that emulate the "static variable" behavior
    of other languages. I do not advise actually using this for anything!!!

    Behavior is intended to be similar to classes that use __slots__. However, "normal"
    attributes and __statics___ can coexist (unlike with __slots__). 

    Example usage: 

        class MyBaseClass(metaclass = StaticVarsMeta):
            __statics__ = {'a','b','c'}
            i = 1 # regular attribute

        class MyParentClass(MyBaseClass):
            __statics__ = {'d','e','f'}
            j = 2 # regular attribute
            d, e, f = 3, 4, 5 # Static vars
            a, b, c = 6, 7, 8 # Static vars (inherited from MyBaseClass, defined here)

        class MyChildClass(MyParentClass):
            __statics__ = {'a','b','c'}
            j = 2 # regular attribute (redefines j from MyParentClass)
            d, e, f = 9, 10, 11 # Static vars (inherited from MyParentClass, redefined here)
            a, b, c = 12, 14, 14 # Static vars (overriding previous definition in MyParentClass here)'''
    statics = {}
    def __new__(mcls, name, bases, namespace):
        # Get the class object
        cls = super().__new__(mcls, name, bases, namespace)
        # Establish the "statics resolution order"
        cls.__sro__ = tuple(c for c in cls.__mro__ if isinstance(c,mcls))

        # Replace class getter, setter, and deleter for instance attributes
        cls.__getattribute__ = StaticVarsMeta.__inst_getattribute__(cls, cls.__getattribute__)
        cls.__setattr__ = StaticVarsMeta.__inst_setattr__(cls, cls.__setattr__)
        cls.__delattr__ = StaticVarsMeta.__inst_delattr__(cls, cls.__delattr__)
        # Store the list of static variables for the class object
        # This list is permanent and cannot be changed, similar to __slots__
        try:
            mcls.statics[cls] = getattr(cls,'__statics__')
        except AttributeError:
            mcls.statics[cls] = namespace['__statics__'] = set() # No static vars provided
        # Check and make sure the statics var names are strings
        if any(not isinstance(static,str) for static in mcls.statics[cls]):
            typ = dict(zip((not isinstance(static,str) for static in mcls.statics[cls]), map(type,mcls.statics[cls])))[True].__name__
            raise TypeError('__statics__ items must be strings, not {0}'.format(typ))
        # Move any previously existing, not overridden statics to the static var parent class(es)
        if len(cls.__sro__) > 1:
            for attr,value in namespace.items():
                if attr not in StaticVarsMeta.statics[cls] and attr != ['__statics__']:
                    for c in cls.__sro__[1:]:
                        if attr in StaticVarsMeta.statics[c]:
                            setattr(c,attr,value)
                            delattr(cls,attr)
        return cls
    def __inst_getattribute__(self, orig_getattribute):
        '''Replaces the class __getattribute__'''
        @wraps(orig_getattribute)
        def wrapper(self, attr):
            if StaticVarsMeta.is_static(type(self),attr):
                return StaticVarsMeta.__getstatic__(type(self),attr)
            else:
                return orig_getattribute(self, attr)
        return wrapper
    def __inst_setattr__(self, orig_setattribute):
        '''Replaces the class __setattr__'''
        @wraps(orig_setattribute)
        def wrapper(self, attr, value):
            if StaticVarsMeta.is_static(type(self),attr):
                StaticVarsMeta.__setstatic__(type(self),attr, value)
            else:
                orig_setattribute(self, attr, value)
        return wrapper
    def __inst_delattr__(self, orig_delattribute):
        '''Replaces the class __delattr__'''
        @wraps(orig_delattribute)
        def wrapper(self, attr):
            if StaticVarsMeta.is_static(type(self),attr):
                StaticVarsMeta.__delstatic__(type(self),attr)
            else:
                orig_delattribute(self, attr)
        return wrapper
    def __getstatic__(cls,attr):
        '''Static variable getter'''
        for c in cls.__sro__:
            if attr in StaticVarsMeta.statics[c]:
                try:
                    return getattr(c,attr)
                except AttributeError:
                    pass
        raise AttributeError(cls.__name__ + " object has no attribute '{0}'".format(attr))
    def __setstatic__(cls,attr,value):
        '''Static variable setter'''
        for c in cls.__sro__:
            if attr in StaticVarsMeta.statics[c]:
                setattr(c,attr,value)
                break
    def __delstatic__(cls,attr):
        '''Static variable deleter'''
        for c in cls.__sro__:
            if attr in StaticVarsMeta.statics[c]:
                try:
                    delattr(c,attr)
                    break
                except AttributeError:
                    pass
        raise AttributeError(cls.__name__ + " object has no attribute '{0}'".format(attr))
    def __delattr__(cls,attr):
        '''Prevent __sro__ attribute from deletion'''
        if attr == '__sro__':
            raise AttributeError('readonly attribute')
        super().__delattr__(attr)
    def is_static(cls,attr):
        '''Returns True if an attribute is a static variable of any class in the __sro__'''
        if any(attr in StaticVarsMeta.statics[c] for c in cls.__sro__):
            return True
        return False
Rick
  • 43,029
  • 15
  • 76
  • 119
1

Your example with MyProperty is not working the way you show it. I suspect a copy/paste error.


Here is a simpler solution using one Static descriptor and one StaticType metaclass:

from types import FunctionType

class Static:
    def __init__(self, value, doc=None):
        self.deleted = False
        self.value = value
        self.__doc__ = doc
    def __get__(self, inst, cls=None):
        if self.deleted:
            raise AttributeError('Attribute not set')
        return self.value
    def __set__(self, inst, value):
        self.deleted = False
        self.value = value
    def __delete__(self, inst):
        self.deleted = True

class StaticType(type):
    def __delattr__(cls, name):
        obj = cls.__dict__.get(name)
        if isinstance(obj, Static):
            obj.__delete__(name)
        else:
            super(StaticType, cls).__delattr__(name)
    def __getattribute__(cls, *args):
        obj = super(StaticType, cls).__getattribute__(*args)
        if isinstance(obj, Static):
            obj = obj.__get__(cls, cls.__class__)
        return obj
    def __setattr__(cls, name, val):
        # check if object already exists
        obj = cls.__dict__.get(name)
        if isinstance(obj, Static):
            obj.__set__(name, val)
        else:
            super(StaticType, cls).__setattr__(name, val)

and in use:

class MyStatic(metaclass=StaticType):
    """
    Testing static vars
    """
    a = Static(9)
    b = Static(12)
    c = 3

class YourStatic(MyStatic):
    d = Static('woo hoo')
    e = Static('doo wop')

and some tests:

ms1 = MyStatic()
ms2 = MyStatic()
ms3 = MyStatic()
assert ms1.a == ms2.a == ms3.a == MyStatic.a
assert ms1.b == ms2.b == ms3.b == MyStatic.b
assert ms1.c == ms2.c == ms3.c == MyStatic.c
ms1.a = 77
assert ms1.a == ms2.a == ms3.a == MyStatic.a
ms2.b = 99
assert ms1.b == ms2.b == ms3.b == MyStatic.b
MyStatic.a = 101
assert ms1.a == ms2.a == ms3.a == MyStatic.a
MyStatic.b = 139
assert ms1.b == ms2.b == ms3.b == MyStatic.b
del MyStatic.b
for inst in (ms1, ms2, ms3):
    try:
        getattr(inst, 'b')
    except AttributeError:
        pass
    else:
        print('AttributeError not raised on %r' % attr)
ms1.c = 13
ms2.c = 17
ms3.c = 19
assert ms1.c == 13
assert ms2.c == 17
assert ms3.c == 19
MyStatic.c = 43
assert ms1.c == 13
assert ms2.c == 17
assert ms3.c == 19

ys1 = YourStatic()
ys2 = YourStatic()
ys3 = YourStatic()
MyStatic.b = 'burgler'
assert ys1.a == ys2.a == ys3.a == YourStatic.a == MyStatic.a
assert ys1.b == ys2.b == ys3.b == YourStatic.b == MyStatic.b
assert ys1.d == ys2.d == ys3.d == YourStatic.d
assert ys1.e == ys2.e == ys3.e == YourStatic.e
ys1.a = 'blah'
assert ys1.a == ys2.a == ys3.a == YourStatic.a == MyStatic.a
ys2.b = 'kelp'
assert ys1.b == ys2.b == ys3.b == YourStatic.b == MyStatic.b
ys1.d = 'fee'
assert ys1.d == ys2.d == ys3.d == YourStatic.d
ys2.e = 'fie'
assert ys1.e == ys2.e == ys3.e == YourStatic.e
MyStatic.a = 'aargh'
assert ys1.a == ys2.a == ys3.a == YourStatic.a == MyStatic.a
Ethan Furman
  • 63,992
  • 20
  • 159
  • 237