You have several questions, so let us take a look at them in the order they were asked:
- The closest thing to a Bagthat you can find in Python's standard library iscollections.Counter. However, it is not designed for concurrent usage and has different methods than theConcurrentBagclass you are familiar with. By inheriting fromcollection.Counterand specifying a custom metaclass designed to make classes thread-safe, it is possible to easily recreate theConcurrentBagclass for use in Python.
- The getter for your thread-safe collection can be duplicated in Python using the propertydecorator. Since you are making the information read-only, this is the easiest method to implement. You can see the property in the code below just below the initializer for theMessageclass.
- Yes, it is possible to check if two objects are equal to each other in Python. You have to define a special __eq__method as part of the class. The version shown in the example code is just below the aforementioned property. It first checks the type of the instance being compared, and then looks through all the other attributes of each instance except for__time.
- Python has no native concept of sealing a class, but a simple metaclass can provide very similar functionality. By setting the metaclass of a class to SealedMeta, any classes that try to inherit from from the "sealed" class will cause aRuntimeErrorto be raised. Please be aware, though, that anyone with access to the source code of your class could remove its sealed designation.
The following classes and metaclasses should solve the issues you were facing. They are as follows:
- SealedMetais a metaclass that can be used to mark a class as being sealed. Any class that tries to inherit from a sealed class will fail to be constructed since a- RuntimeErrorexception will be generated. Because the requirements are so simple, this metaclass is significantly simpler than the second shown in the example code.
- Messageis an attempt at recreating the class you wrote in C#. The arguments and attributes have slightly different names, but the same basic idea is present. Since types are not statically specified in Python, only one equality-checking method is needed in this class. Note how the class is marked as sealed with- (metaclass=SealedMeta)at the top.
- AtomicMetawas a challenging metaclass to write, and I am not sure that it is completely correct. Not only does it have to make the methods of a class thread-safe, it must process all bases that the class inherits from. Since old-style- supercalls may be present in those bases, the metaclass attempts to fix the modules they are located in.
- ConcurrentBaginherits from- collections.Countersince it is most like a bag from other languages. Since you wanted the class to be thread-safe, it must use the- AtomicMetametaclass so that the methods of it and its parents are changed into atomic operations. A- to_arraymethod is introduced to improve its API.
Without further delay, here is the code referenced above. Hopefully, it will be a help both to you and others:
#! /usr/bin/env python3
import builtins
import collections
import functools
import inspect
import threading
class SealedMeta(type):
    """SealedMeta(name, bases, dictionary) -> new sealed class"""
    __REGISTRY = ()
    def __new__(mcs, name, bases, dictionary):
        """Create a new class only if it is not related to a sealed class."""
        if any(issubclass(base, mcs.__REGISTRY) for base in bases):
            raise RuntimeError('no class may inherit from a sealed class')
        mcs.__REGISTRY += (super().__new__(mcs, name, bases, dictionary),)
        return mcs.__REGISTRY[-1]
class Message(metaclass=SealedMeta):
    """Message(identifier, message, source, kind, time) -> Message instance"""
    def __init__(self, identifier, message, source, kind, time):
        """Initialize all the attributes in a new Message instance."""
        self.__identifier = identifier
        self.__message = message
        self.__source = source
        self.__kind = kind
        self.__time = time
        self.__destination = ConcurrentBag()
    @property
    def destination(self):
        """Destination property containing serialized Employee instances."""
        return self.__destination.to_array()
    def __eq__(self, other):
        """Return if this instance has the same data as the other instance."""
        # noinspection PyPep8
        return isinstance(other, type(self)) and \
               self.__identifier == other.__identifier and \
               self.__message == other.__message and \
               self.__source == other.__source and \
               self.__kind == other.__kind and \
               self.__destination == other.__destination
class AtomicMeta(type):
    """AtomicMeta(name, bases, dictionary) -> new thread-safe class"""
    __REGISTRY = {}
    def __new__(mcs, name, bases, dictionary, parent=None):
        """Create a new class while fixing bases and all callable items."""
        final_bases = []
        # Replace bases with those that are safe to use.
        for base in bases:
            fixed = mcs.__REGISTRY.get(base)
            if fixed:
                final_bases.append(fixed)
            elif base in mcs.__REGISTRY.values():
                final_bases.append(base)
            elif base in vars(builtins).values():
                final_bases.append(base)
            else:
                final_bases.append(mcs(
                    base.__name__, base.__bases__, dict(vars(base)), base
                ))
        class_lock = threading.Lock()
        # Wrap all callable attributes so that they are thread-safe.
        for key, value in dictionary.items():
            if callable(value):
                dictionary[key] = mcs.__wrap(value, class_lock)
        new_class = super().__new__(mcs, name, tuple(final_bases), dictionary)
        # Register the class and potentially replace parent references.
        if parent is None:
            mcs.__REGISTRY[object()] = new_class
        else:
            mcs.__REGISTRY[parent] = new_class
            source = inspect.getmodule(parent)
            *prefix, root = parent.__qualname__.split('.')
            for name in prefix:
                source = getattr(source, name)
            setattr(source, root, new_class)
        return new_class
    # noinspection PyUnusedLocal
    def __init__(cls, name, bases, dictionary, parent=None):
        """Initialize the new class while ignoring any potential parent."""
        super().__init__(name, bases, dictionary)
    @staticmethod
    def __wrap(method, class_lock):
        """Ensure that all method calls are run as atomic operations."""
        @functools.wraps(method)
        def atomic_wrapper(self, *args, **kwargs):
            with class_lock:
                try:
                    instance_lock = self.__lock
                except AttributeError:
                    instance_lock = self.__lock = threading.RLock()
            with instance_lock:
                return method(self, *args, **kwargs)
        return atomic_wrapper
# noinspection PyAbstractClass
class ConcurrentBag(collections.Counter, metaclass=AtomicMeta):
    """ConcurrentBag() -> ConcurrentBag instance"""
    def to_array(self):
        """Serialize the data in the ConcurrentBag instance."""
        return tuple(key for key, value in self.items() for _ in range(value))
After doing a little research, you might find that there are alternatives for sealing a class. In Java, classes are marked final instead of using a sealed keyword. Searching for the alternative keyword could bring you to Martijn Pieters' answer which introduces a slightly simpler metaclass for sealing classes in Python. You can use the following metaclass inspired by his the same way that SealedMeta is currently used in the code:
class Final(type):
    """Final(name, bases, dictionary) -> new final class"""
    def __new__(mcs, name, bases, dictionary):
        """Create a new class if none of its bases are marked as final."""
        if any(isinstance(base, mcs) for base in bases):
            raise TypeError('no class may inherit from a final class')
        return super().__new__(mcs, name, bases, dictionary)
The Final metaclass is useful if you want to seal a class and prevent others from inheriting from it, but a different approach is needed if attributes of a class are intended to be final instead. In such a case, the Access metaclass can be rather useful. It was inspired by this answer to the question Prevent function overriding in Python. A modified approach is taken in the metaclass shown below that is designed to take into account attributes of all kinds.
import collections
import threading
# noinspection PyProtectedMember
class RLock(threading._RLock):
    """RLock() -> RLock instance with count property"""
    @property
    def count(self):
        """Count property showing current level of lock ownership."""
        return self._count
class Access(type):
    """Access(name, bases, dictionary) -> class supporting final attributes"""
    __MUTEX = RLock()
    __FINAL = []
    @classmethod
    def __prepare__(mcs, name, bases, **keywords):
        """Begin construction of a class and check for possible deadlocks."""
        if not mcs.__MUTEX.acquire(True, 10):
            raise RuntimeError('please check your code for deadlocks')
        # noinspection PyUnresolvedReferences
        return super().__prepare__(mcs, name, bases, **keywords)
    @classmethod
    def final(mcs, attribute):
        """Record an attribute as being final so it cannot be overridden."""
        with mcs.__MUTEX:
            if any(attribute is final for final in mcs.__FINAL):
                raise SyntaxError('attributes may be marked final only once')
            mcs.__FINAL.append(attribute)
            return attribute
    def __new__(mcs, class_name, bases, dictionary):
        """Create a new class that supports the concept of final attributes."""
        classes, visited, names = collections.deque(bases), set(), set()
        # Find all attributes marked as final in base classes.
        while classes:
            base = classes.popleft()
            if base not in visited:
                visited.add(base)
                classes.extend(base.__bases__)
                names.update(getattr(base, '__final_attributes__', ()))
        # Verify that the current class does not override final attributes.
        if any(name in names for name in dictionary):
            raise SyntaxError('final attributes may not be overridden')
        names.clear()
        # Collect the names of all attributes that are marked as final.
        for name, attribute in dictionary.items():
            for index, final in enumerate(mcs.__FINAL):
                if attribute is final:
                    del mcs.__FINAL[index]
                    names.add(name)
                    break
        # Do a sanity check to ensure this metaclass is being used properly.
        if mcs.__MUTEX.count == 1 and mcs.__FINAL:
            raise RuntimeError('final decorator has not been used correctly')
        mcs.__MUTEX.release()
        dictionary['__final_attributes__'] = frozenset(names)
        return super().__new__(mcs, class_name, bases, dictionary)
As a demonstration of how to use the Access metaclass, this example will raise a SyntaxError:
class Parent(metaclass=Access):
    def __init__(self, a, b):
        self.__a = a
        self.__b = b
    @Access.final
    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