Normally, you can access a regular class attribute/field from an instance of that class. However, when trying to access a class property, an AttributeError is raised. Why can't the instance see the property on the class object?
class Meta(type):
@property
def cls_prop(cls):
return True
class A(metaclass=Meta):
cls_attr = True
A.cls_attr # True
A.cls_prop # True
a = A()
a.cls_attr # True
a.cls_prop # AttributeError: 'A' object has no attribute 'cls_prop'
CodePudding user response:
Error you are seeing is not because of any difference between attribute and property behaviour, but because you define property in Meta and attribute in A class. If you define the property in A it will work just fine.
CodePudding user response:
That is due to the way attribute lookup works. Upon trying to retrieve an attribute in an instance, Python does:
call the instance's class
__getattribute__(not the metaclass__getattribute__), which in turn will:Check the instance's class, and its superclasses following the method resolution order, for the attribute. It does not proceed to the class of the class (the metaclass) - it follows the inheritance chain..
- if the attribute is found in the class and it has a
__get__method, making it a descriptor: the__get__method is called with the instance and its class as parameters - the returned value is used as the attribute value- note: for classes using
__slots__, each instance attribute is recorded in a special descriptor - which exists in the class itself and has a__get__method, so instance attributes for slotted classes are retrieved at this step
- note: for classes using
- if there is no
__get__method, it just skips the search at the class.
- if the attribute is found in the class and it has a
check the instance itself: the attribute should exist as an entry in the instances
__dict__attribute. If so, the corresponding value is returned. (__dict__is an special attribute which is accessed directly in cPython, but would otherwise follow the descriptor rule for slotted attributes, above)The class (and its inheritance hierarchy) are checked again for the attribute, this time, regardless of it having a
__get__method. If found, that is used. This attribute check in the class is performed directly in the class and its superclasses__dict__, not by calling their own__getattribute__in a recursive fashion. (*)
The class (or superclasses)
__getattr__method is called, if it exists, with the attribute name. It may return a value, or raise AttributeError(__getattr__is a different thing from the low level__getattribute__, and easier to customize)AttributeError is raised.
(*) This is the step that answers your question: the metaclass is not searched for an attribute in the instance. In your code above, if you try to use A.cls_prop as a property, instead of A().cls_prop it will work: when retrieving an attribute directly from the class, it takes the role of "instance" in the retrieval algorithm above.
(**) NB. This attribute retrieval algorithm description is fairly complete, but for attribute assignment and deletion, instead of retrieval, there are some differences for a descriptor, based on whether it features a __set__ (or __del__) method, making it a "data descriptor" or not: non-data descriptors (such as regular methods, defined in the instance's class body), are assigned directly on the instance's dict, therefore overriding and "switching off" a method just for that instance. Data descriptors will have their __set__ method called.
how to make properties defined in the metaclass work for instances:
As you can see, attribute access is very customizable, and if you want to define "class properties" in a metaclass that will work from the instance, it is easy to customize your code so that it works. One way is to add to your baseclass (not the metaclass), a __getattr__ that will lookup custom descriptors on the metaclass and call them:
class Base(metaclass=Meta):
def __getattr__(self, name):
metacls = type(cls:=type(self))
if hasattr(metacls, name):
metaattr = getattr(metacls, name)
if isinstance(metaattr, property): # customize this check as you want. It is better not to call it for anything that has a `__get__`, as it would retrieve metaclass specific stuff, such as its __init__ and __call__ methods, if those were not defined in the class.
attr = metaattr.__get__(cls, metacls)
return attr
return super().__getattr__(name)
and:
In [44]: class A(Base):
...: pass
...:
In [45]: a = A()
In [46]: a.cls_prop
Out[46]: True
