Skip to content

Instantly share code, notes, and snippets.

@polyvertex
Last active October 2, 2020 12:33
Show Gist options
  • Save polyvertex/5c306faa7c691697e57aef6b9835aa42 to your computer and use it in GitHub Desktop.
Save polyvertex/5c306faa7c691697e57aef6b9835aa42 to your computer and use it in GitHub Desktop.
__slots__ based class pretty much like a mutable collections.namedtuple
import itertools
import threading
_datastruct_cache_lock = threading.Lock()
_datastruct_cache = {}
class DataStruct(object):
"""
A ``__slots__`` based class that works pretty much like a mutable
`collections.namedtuple`
Values can optionally be passed at construction time to ``__init__`` using
either *args* or *kwargs*.
This class subclasses `object` so to deny the creation of a ``__dict__``.
Usage example::
class ConfigData(DataStruct):
__slots__ = ("url", "user", "proxy")
cfg = ConfigData() # every member defaults to None
# members values can be passed via __init__
cfg = ConfigData("http://example.com", proxy="socks://127.0.0.1")
print(cfg.url) # prints "http://example.com"
print(cfg.user) # prints "None"
print(cfg.proxy) # prints "socks://127.0.0.1"
# it is also possible to derive from your ConfigData class
class ExtraConfigData(ConfigData):
__slots__ = ("timeout", "password", "expected_http_status")
cfg = ExtraConfigData(
"http://example.com", # url
"foo", # user
"socks://127.0.0.1", # proxy
60, # timeout
expected_http_status=200)
print(cfg.password) # prints "None"
"""
__slots__ = ()
def __init__(self, *args, **kwargs):
global _datastruct_cache
with _datastruct_cache_lock:
try:
klasses, all_slots = _datastruct_cache[self.__class__]
except KeyError:
klasses = []
slots_defs = []
for klass in self.__class__.mro():
if klass is DataStruct:
break
if not klass.__slots__:
# raise TypeError(
# f"{klass.__name__}.__slots__ not defined or empty")
continue
klasses.append(klass)
slots_defs.append(klass.__slots__)
# note: reversed() because we want base-to-derived order
klasses = tuple(reversed(klasses))
all_slots = tuple([*itertools.chain(*reversed(slots_defs))])
del slots_defs
if len(all_slots) != len(set(all_slots)):
clsname = self.__class__.__name__
raise ValueError(
f"redundant __slots__ member(s) found in {clsname} "
f"class or any of its bases")
_datastruct_cache[self.__class__] = (klasses, all_slots)
if len(args) > len(all_slots):
raise ValueError("too many positional args passed")
# apply args
for name, value in zip(all_slots, args):
setattr(self, name, value)
# apply kwargs
for name, value in kwargs.items():
if __debug__:
if args and name in all_slots[0:len(args)]:
clsname = self.__class__.__name__
raise ValueError(
f"{clsname}.{name} already init by a positional arg")
try:
setattr(self, name, value)
except AttributeError:
clsname = self.__class__.__name__
raise ValueError(
f"{name} member not found in {clsname}.__slots__, or "
f"any of its sub-classes if any")
def __getattr__(self, name):
with _datastruct_cache_lock:
try:
klasses, all_slots = _datastruct_cache[self.__class__]
except KeyError:
clsname = self.__class__.__name__
raise RuntimeError(
f"no cache for DataStruct-derived class {clsname}")
if name in all_slots:
return None # default value
else:
raise AttributeError(name)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment