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.
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.