1
\$\begingroup\$

Over two years ago, someone asked a question on Stackoverflow about how to seal classes and attributes in Python after coming from a background in C#. The best answer to the question is that Pythonic code does not need or support the notion of preventing children from overriding parent functionality. Yet with metaclasses, people who are set on extending the language to do things it was not designed to can perform all sorts of magic with classes.

Of the the responses to the question, one answer introduces an Access metaclass with a final decorator. It was inspired by another answer but improves upon it by not having the limitation of only be applicable to callables (class methods). Supporting the ability to mark all attributes as final is complicated by the need of comparing objects by their identity and the possibility of threading. Python's threading.RLock had to be extended for a sanity check.

While the best answer to extending the language to support final or sealed classes and attributes may be "just don't do it," learning what is possible in Python and the best way of taking advantage of its capabilities has its benefits. The following metaclass was designed to improve upon previous work as a learning exercise. For the purpose of continued learning, could the code shared below be improved for the purpose of modifying class attributes?

#! /usr/bin/env python3
import collections
import threading


class Modifier(type):
    """Modifier(name, bases, namespace) -> new class instance"""
    __LOCAL = threading.local()

    @classmethod
    def __prepare__(mcs, name, bases, **keywords):
        """Find out what attributes are already sealed for the Namespace."""
        classes, visited, sealed = collections.deque(bases), set(), set()
        while classes:
            base = classes.popleft()
            if base not in visited:
                visited.add(base)
                classes.extend(base.__bases__)
                sealed.update(getattr(base, '__sealed__', ()))
        return Namespace(sealed, mcs.__get_sealed)

    @classmethod
    def __get_sealed(mcs):
        """Ensure the list of sealed attributes exists and return it."""
        try:
            sealed = mcs.__LOCAL.sealed
        except AttributeError:
            sealed = mcs.__LOCAL.sealed = []
        return sealed

    @classmethod
    def sealed(mcs, attribute):
        """Mark an attribute as sealed and check for correct usage."""
        sealed_attributes = mcs.__get_sealed()
        if any(attribute is sealed for sealed in sealed_attributes):
            raise ModifierError(f'attribute {attribute!r} is already sealed')
        sealed_attributes.append(attribute)
        return attribute

    def __new__(mcs, name, bases, namespace):
        """Verify the sealed decorator is used properly and create a class."""
        if mcs.__get_sealed():
            raise ModifierError('sealed decorator was not used properly')
        dictionary = dict(namespace, __sealed__=namespace.new_sealed)
        return super().__new__(mcs, name, bases, dictionary)


class Namespace(dict):
    """Namespace(sealed, get_sealed) -> new Namespace instance"""

    def __init__(self, sealed, get_sealed):
        """Initialize the Namespace to process incoming attributes."""
        super().__init__()
        self.__sealed = sealed
        self.__get_sealed = get_sealed
        self.__new_sealed = set()

    def __setitem__(self, key, value):
        """Check for previously sealed attributes and register new ones."""
        if key in self.__sealed:
            raise ModifierError(f'attribute {key!r} is sealed by parent')
        sealed_attributes = self.__get_sealed()
        for index, sealed in enumerate(sealed_attributes):
            if value is sealed:
                del sealed_attributes[index]
                self.__new_sealed.add(key)
                break
        super().__setitem__(key, value)

    @property
    def new_sealed(self):
        """Property of newly sealed attribute names."""
        return frozenset(self.__new_sealed)


class ModifierError(SyntaxError):
    """ModifierError(*arguments) -> new ModifierError instance"""

Example Usage:

class Parent(metaclass=Modifier):
    def __init__(self, a, b):
        self.__a = a
        self.__b = b

    @Modifier.sealed
    def add(self):
        return self.__a + self.__b

    def sub(self):
        return self.__a - self.__b


class Child(Parent):
    def __init__(self, a, b, c):
        super().__init__(a, b)
        self.__c = c

    def add(self):
        return super().add() + self.__c
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.