Last active
July 23, 2026 07:22
-
-
Save mara004/fd944fe3a4a3fc514171e9cf617292d7 to your computer and use it in GitHub Desktop.
Cached properties with slots, via metaclass (Hacky variant)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # SPDX-FileCopyrightText: 2026 geisserml <geisserml@gmail.com> | |
| # SPDX-License-Identifier: MPL-2.0 | |
| from typing import TYPE_CHECKING | |
| from stl import cached_property | |
| if TYPE_CHECKING: | |
| from functools import cached_property | |
| def _func_visitor(cp_map): | |
| def visit(func): | |
| cp_map[func.__name__] = func | |
| return visit | |
| def _cp_getattr(self, key): | |
| value = self._cp_map_[key](self) | |
| setattr(self, key, value) | |
| return value | |
| class _ClassBuidlerMeta (type): | |
| @classmethod | |
| def __prepare__(metacls, name, bases, use_slots=False, prefer_getattr=None, **kwargs): | |
| nsp = {} | |
| if prefer_getattr or use_slots: | |
| nsp["_cp_map_"] = cp_map = {} | |
| nsp["cached_property"] = _func_visitor(cp_map) | |
| return nsp | |
| def __new__(metacls, clsname, bases, nsp, use_slots=False, prefer_getattr=None, **kwargs): | |
| if use_slots: | |
| fields = nsp.get("_fields_") | |
| nsp["__slots__"] = (*fields, *nsp["_cp_map_"].keys()) | |
| if prefer_getattr or use_slots: | |
| nsp["__getattr__"] = _cp_getattr | |
| for k in nsp["_cp_map_"].keys(): | |
| del nsp[k] | |
| del nsp["cached_property"] | |
| return super().__new__(metacls, clsname, bases, nsp, **kwargs) | |
| class ClassBuilder (metaclass=_ClassBuidlerMeta): | |
| # Child class slots effectively only work when all base classes are slotted, since otherwise there would be an accessible __dict__ attribute. | |
| # That means we have to declare (empty) slots here. This does not negatively impact non-slotted child classes. | |
| # See https://stackoverflow.com/a/28059785/15547292 and https://docs.python.org/3/reference/datamodel.html#slots for more background. | |
| __slots__ = () |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # SPDX-FileCopyrightText: 2026 geisserml <geisserml@gmail.com> | |
| # SPDX-License-Identifier: MPL-2.0 | |
| class cached_property: | |
| def __init__(self, func): | |
| self.func = func | |
| self.assigned_name = None | |
| self.__doc__ = func.__doc__ | |
| # Optional. On older Python versions (< 3.6) that do not call this hook, the func's __name__ will be used as fallback. | |
| def __set_name__(self, cls, name): | |
| if self.assigned_name is None: | |
| self.assigned_name = name | |
| else: | |
| assert name == self.assigned_name, f"A cached property is tied to one attribute. You cannot assign to both {name!r} and {self.assigned_name!r}." | |
| def __get__(self, obj, cls=None): | |
| if obj is None: | |
| return self | |
| value = self.func(obj) | |
| name = self.assigned_name or self.func.__name__ | |
| setattr(obj, name, value) | |
| return value |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # SPDX-FileCopyrightText: 2026 geisserml <geisserml@gmail.com> | |
| # SPDX-License-Identifier: MPL-2.0 | |
| import sys | |
| import uuid | |
| from pathlib import Path | |
| from class_kit_old import * # local | |
| # USE_SLOTS, PREFER_GETATTR = False, None | |
| # USE_SLOTS, PREFER_GETATTR = False, True | |
| USE_SLOTS, PREFER_GETATTR = True, None | |
| if USE_SLOTS: | |
| def get_vars(obj): | |
| return {k: getattr(d, k) for k in d._fields_} | |
| else: | |
| get_vars = vars # builtin | |
| class TestData (ClassBuilder, use_slots=USE_SLOTS, prefer_getattr=PREFER_GETATTR): | |
| _fields_ = ("file_name", ) | |
| def __init__(self, file_name): | |
| self.file_name = file_name | |
| @cached_property | |
| def file_content(self): | |
| print(f"Reading file {self.file_name}", file=sys.stderr) | |
| return Path(self.file_name).read_text() | |
| @cached_property | |
| def unique_id(self): | |
| print(f"Generating unique id", file=sys.stderr) | |
| return uuid.uuid4() | |
| d = TestData(__file__) | |
| print(d) | |
| print(get_vars(d)) | |
| if USE_SLOTS or PREFER_GETATTR: | |
| print(TestData.__getattr__) | |
| print(TestData._cp_map_) | |
| if USE_SLOTS: | |
| print(d.__slots__) | |
| try: | |
| d.nonexistent = "test" | |
| except AttributeError: | |
| assert USE_SLOTS, "assigned new attribute, but slotting was expected" | |
| print("Confirmed the class is slotted") | |
| else: | |
| assert not USE_SLOTS, "failed to assign new attribute, but slotting was not expected" | |
| print("Confirmed the class is non-slotted") | |
| print(d.unique_id) | |
| print(d.unique_id) | |
| del d.unique_id | |
| print(d.unique_id) | |
| print(d.file_content[:10]) | |
| print(d.file_content[:10]) | |
| del d.file_content | |
| print(d.file_content[:10]) |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
I find this approach a little cleaner: https://gist.github.com/mara004/076e3110aba8e8a3f8a6f2c84af350ee