3

Python in a Nutshell describes the lookup procedures when getting an attribute. The book distinguishes two cases

  • the lookup procedure when getting an attribute from a class, e.g. cls.name

    Getting an attribute from a class

    When you use the syntax C.name to refer to an attribute on a class object C, the lookup proceeds in two steps:

    1. When name is a key in C.__dict__, C.name fetches the value v from C.__dict__['name'] . Then, when v is a descriptor (i.e., type(v) supplies a method named __get__ ), the value of C.name is the result of calling type(v).__get__(v, None, C) . When v is not a descriptor, the value of C.name is v .

    2. When name is not a key in C.__dict__ , C.name delegates the lookup to C ’s base classes, meaning it loops on C ’s ancestor classes and tries the name lookup on each (in method resolution order, as covered in “Method resolution order” on page 113).

  • the lookup procedure when getting an attribute from an instance, e.g. obj.name

Since in Python 3, every class object is actually an instance of its metaclass (e.g. type class), according to the book, why are the lookup procedure for getting an attribute from a class and the lookup procedure for getting an attribute from an instance different?

Christian Dean
  • 22,138
  • 7
  • 54
  • 87
Tim
  • 1
  • 141
  • 372
  • 590

1 Answers1

4

They're not very different, and the description from the book covers the two ways in which they differ:

  1. Descriptors found on a class instance (after not being found on the class) don't get invoked (a.x = somedescriptor() where a is a class instance, not a class, followed by a.x will just return the instance of somedescriptor() you just made), while descriptors found on a metaclass instance i.e. a class (after not being found on the metaclass) get invoked with None as the instance it was called on (A.x = somedescriptor() where A is a metaclass instance, not a metaclass, will invoke .__get__(None, A) on the somedescriptor() you just made). This allows stuff like @classmethod to work by binding the method to the class itself whether it's looked up on an instance of the class or the class itself.
  2. Class instances don't have a concept of "parent instances" (the namespace of the class instance itself is a flat dict, even if the attributes associated with that class instance were defined by methods from multiple levels of inheritance), so the idea of MRO-based lookup is unique to metaclass instances.

Everything else is pretty much the same, it's just that the book is glossing over the concept of metaclasses here, since most classes are instances of the base type, which has no special behaviors. If you have a metaclass other than type, the full instance lookup rules apply when looking up attributes on a class (it's just the class of the class is the metaclass).

They were probably trying to avoid the complexity of metaclasses early on, but in the process made it seem like the rules for instance lookup didn't apply to classes; they do, it's just that classes add a couple extra rules to the basic lookup procedure.

Géry Ogam
  • 6,336
  • 4
  • 38
  • 67
ShadowRanger
  • 143,180
  • 12
  • 188
  • 271
  • Thanks! Do you mean the lookup procedure for getting an attribute from a class in the book only works when the class is an instance of `type` class? – Tim Jul 06 '17 at 01:24
  • May I also ask what you think about [setting attribute](https://stackoverflow.com/questions/44937897/what-is-the-lookup-procedure-when-setting-an-attribute-from-a-class-or-from-an-i)? – Tim Jul 06 '17 at 01:26
  • 1
    @Tim: It means the rules apply as written when it's an instance of `type`, but if it has some other metaclass, the rules *could* invoke all the behaviors of "normal" instances. A really simple metaclass could end up behaving the same as `type` (since it's a subclass of `type`, it need not differ at all), a more complex one might define all the little quirky things that make instances more complicated. – ShadowRanger Jul 06 '17 at 01:27
  • By "it's a subclass of `type`", do you mean "it's an instance of `type`" instead (I guess "it" mean "a really simple metaclass")? If a really simple metaclass doesn't differ at all from `type`, what is the reason to use the really simple metaclass instead of using `type` directly as the metaclass? – Tim Jul 06 '17 at 01:29
  • @Tim: It's the same thing for setting attributes. For a normal, non-metaclass based class, the simple rules are there to avoid complexity, but if the metaclass does arbitrarily weird things (e.g. it has it's own descriptors defined, or `__setattr__` or whatever), then operations on the class that has it as a metaclass will be affected the same way an instance of a class is affected. – ShadowRanger Jul 06 '17 at 01:30
  • @Tim: There isn't a reason for an empty `type` subclass. It's for illustrative purposes. If you make `class JunkType(type): pass`, then aside from breaking some interpreter optimizations (requiring the interpreter to do all the lookups `type` itself can dodge), the observed behavior of a class with `JunkType` as a metaclass continues to match the simplified description given. If you add a class attribute, descriptor, `__setattr__`, what-have-you, to the metaclass, well, now it doesn't match, but you don't have to do that. – ShadowRanger Jul 06 '17 at 01:33
  • Some early uses of metaclasses were roughly equivalent to a class decorator (they did some post-processing of the class at definition time, e.g. manually injecting some extra method, but didn't actually behave differently from `type` after the class was defined), so in those cases, the simplified rules continue to be useful. – ShadowRanger Jul 06 '17 at 01:34
  • A metaclass always subclasses `type`, that's what makes it a metaclass and not just a class. I don't mean it's an instance of `type` (though it is, because it's a `class`, and all classes are instances of `type` or one of its subclasses, including metaclasses, but that's trivially true). – ShadowRanger Jul 06 '17 at 01:37
  • The rules for attribute lookup *are* different for instances (they are in `object.__getattribute__`) and classes (they are in `type.__getattribute__`): before trying to return `vars(A)['x']`, the attribute lookup `A.x` from a class `A` will try to return `vars(A)['x'].__get__(None, A)` if `A` has a class attribute `x` and if this attribute has a `__get__` method. This step does not happen for attribute lookup `a.x` from an instance `a` of `A`. – Géry Ogam Mar 22 '21 at 18:38
  • 1
    @Maggyero: Ah, you're right. Took me a moment to figure out what you were saying (I initially misread it as a claim about the descriptor protocol not being invoked on the metaclass when accessing stuff on the classes using it). You can't make a regular instance invoke the descriptor protocol on per-instance attributes (saying `a.x = property(somefunc)` won't make `a.x` do anything but return the raw `property` object), but it happens on per-class attributes when the metaclass doesn't provide an override via a descriptor. I'll tweak the answer to cover this. – ShadowRanger Mar 23 '21 at 02:20
  • @Maggyero: Fixed. Feel free to suggest additional fixes if I missed something. I think the summary of "class lookups are the same as instance lookups, with a couple additional rules" is roughly on target. – ShadowRanger Mar 23 '21 at 02:39
  • You put into words the extra rule for attribute lookup on classes way better than me. And your remark on the practical application of this rule for `classmethod` is very insightful. – Géry Ogam Mar 23 '21 at 09:33