Search code examples
pythonpython-2.7metaclassreadonly-attribute

Create first-class object all of it's instance attributes are readonly like slice?


My question is how to create a class like slice?

slice (built-in type) doesn't have a __dict__ attribute even that the metaclass of this slice is type.

And it is not using __slots__, and all it's attribute are readonly and it's not overriding __setattr__ (this i'm not sure about it but look at my code and see if I'm right).

Check this code:

# how slice is removing the __dict__ from the class object
# and the metaclass is type!!

class sliceS(object):
    pass

class sliceS0(object):

    def __setattr__(self, name, value):
        pass

# this means that both have the same
# metaclass type.
print type(slice) == type(sliceS) # prints True

# from what i understand the metaclass is the one
# that is responsible for making the class object
sliceS2 = type('sliceS2', (object,), {})
# witch is the same
# sliceS2 = type.__new__(type, 'sliceS2', (object,), {})
print type(sliceS2) # prints type

# but when i check the list of attribute using dir
print '__dict__' in dir(slice)  # prints False
print '__dict__' in dir(sliceS) # prints True

# now when i try to set an attribute on slice
obj_slice = slice(10)
# there is no __dict__  here
print '__dict__' in dir(obj_slice) # prints False
obj_sliceS = sliceS()
try:
    obj_slice.x = 1
except AttributeError as e:
    # you get AttributeError
    # mean you cannot add new properties
    print "'slice' object has no attribute 'x'"

obj_sliceS.x = 1 # Ok: x is added to __dict__ of obj_sliceS
print 'x' in obj_sliceS.__dict__ # prints True

# and slice is not using __slots__ because as you see it's not here
print '__slots__' in dir(slice) # print False

# and this why i'm saying it's not overriding the __settattr__
print id(obj_slice.__setattr__) == id(obj_sliceS.__setattr__) # True: it's the same object
obj_sliceS0 = sliceS0()
print id(obj_slice.__setattr__) == id(obj_sliceS0.__setattr__) # False: it's the same object

# so slice have only start, stop, step and are all readonly attribute and it's not overriding the __setattr__
# what technique it's using?!!!!

How to make this kind of first-class object all of it's attributes are readonly and you cannot add new attributes.


Solution

  • The thing is that Python's built-in slice class is programmed in C. And when you code using the C-Python API you can code the equivalent of attributes accessible with the __slots__ without using any mechanisms visible from the Python side. (You can even have 'real' private attributes, which are virtually impossible with Python only code).

    The mechanism used for Python code to be able to prevent a __dict__ for a class' instances and subsequent "any attribute can be set" is the __slots__ exactly the attribute. However, unlike magic dunder methods that have to be present when the class is actually used, the information on __slots__ is used when the class is created, and only then. So, if what concerns you is to have a visible __slots__ in your final class, you can just remove it from the class before exposing it:

    In [8]: class A:
       ...:     __slots__ = "b"
       ...:     
    
    In [9]: del A.__slots__
    
    In [10]: a = A()
    
    In [11]: a.b = 5
    
    In [12]: a.c = 5
    ------------------------
    AttributeError   
    ...
    
    In [13]: A.__slots__
    ---------------------------------------------------------------------------
    AttributeError                            Traceback (most recent call last)
    <ipython-input-13-68a69c802e74> in <module>()
    ----> 1 A.__slots__
    
    AttributeError: type object 'A' has no attribute '__slots__'
    

    If you won't like a del MyClass.__slots__ line to be visible wherever you declare a class, it is a one-line class decorator:

    def slotless(cls):
       del cls.__slots__
       return cls
    
    @slotless
    class MyClass:
       __slots__ = "x y".split()
    

    Or, you could use a metaclass to auto-create, and auto-destroy the Python visible __slots__, so that you could declare your descriptors and attributes in the class body, and have the class protected against extra attributes:

    class AttrOnly(type):
       def __new__(metacls, name, bases, namespace, **kw):
            namespace["__slots__"] = list(namespace.keys())  # not sure if "list(" is needed
            cls = super().__new__(metacls, name, bases, namespace, **kw)
            del cls.__slots__
            return cls
    
    class MyClass(metaclass=AttrOnly):
        x = int
        y = int
    

    If you want pure Python readonly attributes which does not have a visible counterpart in the instance itself (like a ._x which is used by a property descriptor to keep the value of a x attribute), the straightforward way is to customize __setattr__ . Another approach is to have your metaclass to auto-add a read-only property for each attribute on the class creation stage. The metaclass bellow does that and uses the __slots__ class attribute to create the desired descriptors:

    class ReadOnlyAttrs(type):
        def __new__(metacls, name, bases, namespace, **kw):
            def get_setter(attr):
                def setter(self, value):
                    if getattr(self, "_initialized", False): 
                        raise ValueError("Can't set "  + attr)
                    setattr(self, "_" + attr, value)
                return setter
    
            slots = namespace.get("__slots__", [])
            slots.append("initialized")
            def __new__(cls, *args, **kw):
                self = object.__new__(cls)  # for production code that could have an arbitrary hierarchy, this needs to be done more carefully
                for attr, value in kw.items():
                    setattr(self, attr, value)
                self.initialized = True
                return self
    
            namespace["__new__"] = __new__
            real_slots = []
            for attr in slots:
                real_slots.append("_" + attr)
                namespace[attr] = property(
                    (lambda attr: lambda self: getattr(self, "_" + attr))(attr), # Getter. Extra lambda needed to create an extra closure containing each attr
                    get_setter(attr)
                )
            namespace["__slots__"] = real_slots
            cls = super().__new__(metacls, name, bases, namespace, **kw)
            del cls.__slots__
            return cls
    

    Have in mind you can also customize the class' __dir__ method so that _x shadow attributes would not be seen, if you want to.