Back when I wrote some notes on
__slots__
and class hierarchies , I said in passing that there was no simple way to see what attributes an object currently has (I was sort of talking about objects that use
__slots__
, but it's actually more general). Today, for reasons beyond the scope of this entry, I feel like talking about why things work out this way.
To see where things get tricky, I'll start out by talking about where they're simple. If what we have is some basic struct object and we want to see what fields it has, the most straightforward approach is to look at its
__dict__
. We can get the same result indirectly by taking the
dir()
of the object and subtracting the
dir()
of its class:
>>> class A:
... def __init__(self):
... self.a = 10
... self.b = 20
...
>>> a = A()
>>> set(dir(a)) - set(dir(a.__class__))
{'b', 'a'}
(This falls out of the definition of
dir()
, but note that this only works on simple objects that don't do a variety of things.)
The first problem is that neither version of this approach works for instances of classes that use
__slots__
. Such objects have no
__dict__
, and if you look at
dir()
it will tell you that they have no attributes of their own:
>>> class B:
... __slots__ = ('a', 'b')
... def __init__(self):
... self.a = 10
...
>>> b = B()
>>> set(dir(b)) - set(dir(b.__class__))
set()
This follows straightforwardly from how __slots__ are defined , particularly this bit:
__slots__are implemented at the class level by creating descriptors ( Implementing Descriptors ) for each variable name. [...]
Descriptors are attributes on the class, not on instances of the class, although they create behavior in those instances. As we can see in
dir()
, the class itself has
a
and
b
attributes:
>>> B.a <member 'a' of 'B' objects>
(In CPython, these are
member_descriptor
objects .)
For an instance of a
__slots__
using class, we still have a somewhat workable definition of what attributes it has. For each
__slots__
attribute, an instance has the attribute if
hasattr()
is true for it, which means that you can access it. Here our
b
instance of
B
has an
a
attribute but doesn't have a
b
attribute. You can at least write code that mechanically checks this, although it's a bit harder than it looks.
(One part is that you need the union of
__slots__
on all base classes.)
However, we've now arrived at the tricky bit. Suppose that we have a general property on a class under the name
par
. When should we say that instances of this class have a
par
attribute? In one sense, instances never will, because at the mechanical level
par
will always be a class attribute and will never appear in an instance
__dict__
. In another sense, we could reasonably say that instances have a
par
attribute when
hasattr()
is true for it, ie when accessing
inst.par
won't raise
AttributeError
; this is the same definition as we used for
__slots__
attributes. Or we might want to be more general and say that an attribute only 'exists' for our purposes when accessing it doesn't raise any errors, not just
AttributeError
(after all, this is when we can use the attribute). But what if this property actually computes the value for
par
on the fly from somewhere, in effect turning an attribute into a method; do we say that
par
is still an attribute of the instance, even though it doesn't really act like an attribute any more?
Python has a lot of ways to attach sophisticated behavior to instances of classes that's triggered when you try to access an attribute in some way. Once we have such sophisticated behavior in action, there's no clear or universal definition of when an instance 'has' an attribute and it becomes a matter of interpretation and opinion. This is one deep cause of why there's no simple way to see what attributes an object currently has; once we get past the simple cases, it's not even clear what the question means.
(Even if we come up with a meaning for ourselves, classes that define
__getattr__
or
__getattribute__
make it basically impossible to see what attribute names we want to check, as the
dir()
documentation gently notes. There are many complications here.)
Sidebar: The pragmatic answer
The pragmatic answer is that if it's sensible to ask this question about an object at all, we can get pretty much the answer we want by looking at the object's
__dict__
(if it has one), then adding the merged
__slots__
names for which
hasattr()
reports true.
That this answer blows up on things like proxy objects suggests that perhaps it's not a question we should be asking in the first place, at least not outside of limited and specialized situations.
(In other words, it's possible to get entirely too entranced with the theory of Python and neglect its practical applications. I'm guilty of this from time to time.)