Reputation: 2427
This may appear as a very basic question, but I couldn't find anything helpful on SO or elsewhere...
If you take built-in classes, such as int
or list
, there is no way to create additional class attributes for them (which is obviously a desirable behavior) :
>>> int.x = 0
Traceback (most recent call last):
File "<pyshell#16>", line 1, in <module>
int.x = 0
TypeError: can't set attributes of built-in/extension type 'int'
but if you create your own custom class, this restriction is not actived by default, so anybody may create additional class attributes in it
class foo(object):
a = 1
b = 2
>>> foo.c = 3
>>> print(foo.a, foo.b, foo.c)
1 2 3
I know that the __slots__
class attribute is one solution (among others) to forbid creation of unwanted instance attributes, but what is the process to forbid unwanted class attributes, as done in the built-in classes ?
Upvotes: 4
Views: 987
Reputation: 69031
Whenever you see built-in/extension type
you are dealing with an object that was not created in Python. The built-in
types of CPython were created with C
, for example, and so the extra behavior of assigning new attributes was simply not written in.
You see similar behavior with __slots__
:
>>> class Huh:
... __slots__ = ('a', 'b')
>>> class Hah(Huh):
... pass
>>> Huh().c = 5 # traceback
>>> Hah().c = 5 # works
As far as making Python classes immutable, or at least unable to have new attributes defined, a metaclass
is the route to go -- although anything written in pure Python will be modifiable, it's just a matter of how much effort it will take:
>>> class A(metaclass=FrozenMeta):
... a = 1
... b = 2
>>> type.__setattr__(A, 'c', 9)
>>> A.c
9
A more complete metaclass:
class Locked(type):
"support various levels of immutability"
#
def __new__(metacls, cls_name, bases, clsdict, create=False, change=False, delete=False):
cls = super().__new__(metacls, cls_name, bases, {
"_Locked__create": True,
"_Locked__change": True,
"_Locked__delete": True,
**clsdict,
})
cls.__create = create
cls.__change = change
cls.__delete = delete
return cls
#
def __setattr__(cls, name, value):
if hasattr(cls, name):
if cls.__change:
super().__setattr__(name, value)
else:
raise TypeError('%s: cannot modify %r' % (cls.__name__, name))
elif cls.__create:
super().__setattr__(name, value)
else:
raise TypeError('%s: cannot create %r' % (cls.__name__, name))
#
def __delattr__(cls, name):
if not hasattr(cls, name):
raise AttributeError('%s: %r does not exist' % (cls.__name__, name))
if not cls.__delete or name in (
'_Locked__create', '_Locked__change', '_Locked_delete',
):
raise TypeError('%s: cannot delete %r' % (cls.__name__, name))
super().__delattr__(name)
and in use:
>>> class Changable(metaclass=Locked, change=True):
... a = 1
... b = 2
...
>>> Changable.a = 9
>>> Changable.c = 7
Traceback (most recent call last):
...
TypeError: Changable: cannot create 'c'
>>> del Changable.b
Traceback (most recent call last):
...
TypeError: Changable: cannot delete 'b'
Upvotes: 0
Reputation: 106455
@AlexisBRENON's answer works but if you want to emulate the behavior of a built-in class, where subclasses are allowed to override attributes, you can set the __frozen
attribute to True
only when the bases
argument is empty:
class FrozenMeta(type):
def __new__(cls, name, bases, dct):
inst = super().__new__(cls, name, bases, {"_FrozenMeta__frozen": False, **dct})
inst.__frozen = not bases
return inst
def __setattr__(self, key, value):
if self.__frozen and not hasattr(self, key):
raise TypeError("I am frozen")
super().__setattr__(key, value)
class A(metaclass=FrozenMeta):
a = 1
b = 2
class B(A):
pass
B.a = 2
B.c = 1 # this is OK
A.c = 1 # TypeError: I am frozen
Upvotes: 1
Reputation: 3079
I think you should play with metaclasses. It can define the behavior of your class instead of its instances.
The comment from Patrick Haugh refers to another SO answer with the following code snippet:
class FrozenMeta(type):
def __new__(cls, name, bases, dct):
inst = super().__new__(cls, name, bases, {"_FrozenMeta__frozen": False, **dct})
inst.__frozen = True
return inst
def __setattr__(self, key, value):
if self.__frozen and not hasattr(self, key):
raise TypeError("I am frozen")
super().__setattr__(key, value)
class A(metaclass=FrozenMeta):
a = 1
b = 2
A.a = 2
A.c = 1 # TypeError: I am frozen
Upvotes: 2