(Ab)using the import system
Python already has a compact destructuring syntax in the form of from x import y. This can be re-purposed to destructure dicts and objects:
import sys, types
class MyClass:
def __init__(self, a, b):
self.a = a
self.b = b
sys.modules["myobj"] = MyClass(1, 2)
from myobj import a, b
assert a + b == 3
mydict = {"c": 3, "d": 4}
sys.modules["mydict"] = types.SimpleNamespace(**mydict)
from mydict import c, d
assert c + d == 7
Cluttering sys.modules with our objects isn't very nice though.
Context manager
A more serious hack would be a context manager that temporarily adds a module to sys.modules, and makes sure the __getattr__ method of the module points to the __getattribute__ or __getitem__ method of the object/dict in question.
That would let us do:
mydict = {"a": 1, "b": 2}
with obj_as_module(mydict, "mydict"):
from mydict import a, b
assert a + b == 3
assert "mydict" not in sys.modules
Implementation:
import sys, types
from contextlib import contextmanager
@contextmanager
def obj_as_module(obj, name):
"Temporarily load an object/dict as a module, to import its attributes/values"
module = types.ModuleType(name)
get = obj.__getitem__ if isinstance(obj, dict) else obj.__getattribute__
module.__getattr__ = lambda attr: get(attr) if attr != "__path__" else None
try:
if name in sys.modules:
raise Exception(f"Name '{name}' already in sys.modules")
else:
sys.modules[name] = module
yield module
finally:
if sys.modules[name] == module:
del sys.modules[name]
This was my first time playing around with the import system, and I have no idea if this might break something, or what the performance is like. But I think it is a valuable observation that the import statement already provides a very convenient destructuring syntax.
Replacing sys.modules entirely
Using an even more questionable hack, we can arrive at an even more compact syntax:
with from_(mydict): import a, b
Implementation:
import sys
@contextmanager
def from_(target):
"Temporarily replace the sys.modules dict with target dict or it's __dict__."
if not isinstance(target, dict):
target = target.__dict__
sysmodules = sys.modules
try:
sys.modules = target
yield
finally:
sys.modules = sysmodules
Class decorator
For working with classes we could use a decorator:
def self_as_module(cls):
"For those who like to write self-less methods"
cls.as_module = lambda self: obj_as_module(self, "self")
return cls
Then we can unpack attributes without cluttering our methods with lines like a = self.a:
@self_as_module
class MyClass:
def __init__(self):
self.a = 1
self.b = 2
def check(self):
with self.as_module():
from self import a, b
assert a + b == 3
MyClass().check()
For classes with many attributes and math-heavy methods, this is quite nice.
Keyword arguments
By using keyword arguments we can save on typing the string quotes, as well as loading multiple modules in one go:
from contextlib import ExitStack
class kwargs_as_modules(ExitStack):
"If you like 'obj_as_module', but want to save even more typing"
def __init__(self, **kwargs):
super().__init__()
for name, obj in kwargs.items():
self.enter_context(obj_as_module(obj, name))
Test:
myobj = types.SimpleNamespace(x=1, y=2)
mydict = {"a": 1, "b": 2}
with kwargs_as_modules(one=myobj, two=mydict):
from one import a, b
from two import x, y
assert a == x, b == y