7

Today I stumbled upon the following behaviour:

class myobject(object):
    """Should behave the same as object, right?"""

obj = myobject()
obj.a = 2        # <- works
obj = object()
obj.a = 2        # AttributeError: 'object' object has no attribute 'a'

I want to know what is the logic behind designing the language to behave this way, because it feels utterly paradoxical to me. It breaks my intuition that if I create a subclass, without modification, it should behave the same as the parent class.


EDIT: A lot of the answers suggest that this is because we want to be able to write classes that work with __slots__ instead of __dict__ for performance reasons. However, we can do:

class myobject_with_slots(myobject):
    __slots__ = ("x",)
    
obj = myobject_with_slots()
obj.x = 2
obj.a = 2
assert "a" in obj.__dict__      # ✔
assert "x" not in obj.__dict__  # ✔

So it seems we can have both __slots__ and __dict__ at the same time, so why doesn't object allow both, but one-to-one subclasses do?

Hyperplane
  • 1,422
  • 1
  • 14
  • 28
  • 3
    `object()` does not have a `__dict__`, but your subclass does. :) Is "something something performance" satisfying enough as an answer? – timgeb Jan 18 '22 at 09:23
  • @timgeb "Is 'something something performance' satisfying enough as an answer?" Not really, maybe you could expand on that? – Hyperplane Jan 18 '22 at 09:27
  • 2
    Related question: https://stackoverflow.com/questions/44880168/object-does-not-have-a-dict-so-you-can-t-assign-arbitrary-attributes-to-an . The last paragraph in the accepted answer gives the reason why `object` instances do no have a `__dict__` attribute (also given in the answers below by MisterMiyagi and Alex Hall). – 9769953 Jan 18 '22 at 09:27

3 Answers3

5

Consider this code:

class A:
    __slots__ = ()

class B(A):
    __slots__ = ("x", "y")

b = B()
b.z = 1  # AttributeError

__slots__ = ("x", "y") means that B instances don't have a __dict__ and can only have attributes x and y. This is good for performance.

If you remove the __slots__ from A, then A instances get a __dict__, which means so do their subclasses, particularly B. This reduces the effectiveness of __slots__ at improving performance.

Because object is a superclass of all classes, you can think of it like A here, having __slots__ = () and no __dict__ so that other classes can also avoid having a __dict__ and fully benefit from custom __slots__.

Alex Hall
  • 34,833
  • 5
  • 57
  • 89
  • OK, that makes sense. But it still feels like this must be breaking some principles. For one, it seems weird that subclasses would automatically switch their behaviour to `__dict__`, why not have a dedicated subclass of `object` for that? Secondly, one can still create a subclass of a class with `__dict__` that has `__slots__`, but python doesn't produce any warnings/errors that this wouldn't work as intended. – Hyperplane Jan 18 '22 at 09:43
  • @Hyperplane The point of ``__slots__`` is not *just* to prevent arbitrary attributes. It also reduces memory footprint and improves performance – even if there's a ``__dict__`` slot as well. – MisterMiyagi Jan 18 '22 at 09:55
  • @MisterMiyagi I think OP is saying it's weird that a class declaring slots doesn't actually have slots unless all its base classes do as well. It's not about having a slot called `__dict__`. I agree that it's very weird. – Alex Hall Jan 18 '22 at 09:59
  • 1
    @AlexHall I just tried creating a subclass of `myobject` with `__slots__=("x",)`, and it does seem to use slots for the names specified in `__slots__`, .e.g. after `obj.x=2`, `"x"` does not appear in `obj.__dict__`, but after calling `obj.a=2`, then `obj.__dict__ ={'a': 2}`. – Hyperplane Jan 18 '22 at 10:03
  • What does "the slots in B stop working" mean? – Kelly Bundy Jan 18 '22 at 10:07
4

Because derived classes do not necessarily support setattr either.

class myobject(object):
    """Should behave the same as object!"""
    __slots__ = ()

obj = myobject()
obj.a = 2        # <- works the same as for object

Since all types derive from object, most builtin types such as list are also examples.

Arbitrary attribute assignment is something that object subclasses may support, but not all do. Thus, the common base class does not support this either.


Support for arbitrary attributes is commonly backed by the so-called __dict__ slot. This is a fixed attribute that contains a literal dict 1 to store any attribute-value pairs.

In fact, one can manually define the __dict__ slot to get arbitrary attribute support.

class myobject(object):
    """Should behave the same as object, right?"""
    __slots__ = ("__dict__",)

obj = myobject()
obj.a = 2            # <- works!
print(obj.__dict__)  # {'a': 2}

The takeaway from this demonstration is that fixed attributes is actually the "base behaviour" of Python; the arbitrary attributes support is built on top when required.

Adding arbitrary attributes for object subtypes by default provides a simpler programming experience. However, still supporting fixed attributes for object subtypes allows for better memory usage and performance.

Data Model: __slots__

The space saved [by __slots__] over using __dict__ can be significant. Attribute lookup speed can be significantly improved as well.

Note that it is possible to define classes with both fixed attributes and arbitrary attributes. The fixed attributes will benefit from the improved memory layout and performance; since they are not stored in the __dict__, its memory overhead2 is lower – but it still costs.


1Python implementations may use different, optimised types for __dict__ as long as they behave like a dict.

2For its hash-based lookup to work efficiently with few collisions, a dict must be larger than the number of items it stores.

MisterMiyagi
  • 44,374
  • 10
  • 104
  • 119
2

Short answer

object() by default does not have an attribute dictionary (__dict__). It allows the object() class and anything that inherits from it to save a few bytes.

Why is it so important?

Every class in Python inherits from object(). Classes like str, dict, tuple and int are used endlessly both internally and externally.

Having an instance dictionary means that every object in Python will be both larger (consume more memory) and slower (every attribute will cause a dictionary lookup).

In order to improve flexibility, by default, user-created classes do come with an instance __dict__. It allows us to patch instances, hook on methods, dynamically inject dependencies and offers an endless amount of different benefits. It is what gives Python its strength as a dynamic programming language and one of the paramount reasons for its success.

To prevent creating one, you may set __slots__ like so:

class A:
    __slots__ = ()

A().abc = 123  # Will throw an error

Having no instance dictionary means that regular attribute access can skip searching __dict__. The faster attribute access leads to a large overall improvement in the Python runtime, but will reduce flexibility in your class usage.

The way attribute lookup works without using __dict__ is out of scope of the question. You may read more about __slots__ in the documentation.


For your second question:

Any user-made class that doesn't have __slots__ has an instance dictionary (__dict__).

If you subclass it, you can't add __slots__ and remove the dictionary of the parent class, it already exists.

Having both __slots__ and a dictionary removes most of the upsides of using __slots__ which is saving space and preventing a dictionary creation.

>>> import sys
>>> class A:
...  pass
...
>>> class B:
...  __slots__ = ()
...
>>> sys.getsizeof(A())
48
>>> sys.getsizeof(B())
32
>>> class C(A):
...  __slots__ = ()
...
>>> sys.getsizeof(C())
48
>>> C.__dict__
mappingproxy({'__module__': '__main__', '__slots__': (), '__doc__': None})
Bharel
  • 23,672
  • 5
  • 40
  • 80
  • > *"By default, user-created classes do come with an instance `__dict__`. In order to prevent creating one, you may set `__slots__` like so"* I have to say, it feels weird that one has to add extra things to what is effectively an empty subclass if one wants it to behave the same as the parent class. – Hyperplane Jan 18 '22 at 09:52
  • @Hyperplane True, the improved flexibility however is paramount to Python as a language. It allows you to implement plenty of designs right off the bat. Monkey-patching, dependency injection, caching... – Bharel Jan 18 '22 at 09:57