What is a DynamicClassAttribute and how do I use it?

后端 未结 2 434
刺人心
刺人心 2021-02-02 09:53

As of Python 3.4, there is a descriptor called DynamicClassAttribute. The documentation states:

types.DynamicClassAttribute(fget=None, fset=None, fdel

2条回答
  •  小蘑菇
    小蘑菇 (楼主)
    2021-02-02 10:09

    New Version:

    I was a bit disappointed with the previous answer so I decided to rewrite it a bit:

    First have a look at the source code of DynamicClassAttribute and you'll probably notice, that it looks very much like the normal property. Except for the __get__-method:

    def __get__(self, instance, ownerclass=None):
        if instance is None:
            # Here is the difference, the normal property just does: return self
            if self.__isabstractmethod__:
                return self
            raise AttributeError()
        elif self.fget is None:
            raise AttributeError("unreadable attribute")
        return self.fget(instance)
    

    So what this means is that if you want to access a DynamicClassAttribute (that isn't abstract) on the class it raises an AttributeError instead of returning self. For instances if instance: evaluates to True and the __get__ is identical to property.__get__.

    For normal classes that just resolves in a visible AttributeError when calling the attribute:

    from types import DynamicClassAttribute
    class Fun():
        @DynamicClassAttribute
        def has_fun(self):
            return False
    Fun.has_fun
    

    AttributeError - Traceback (most recent call last)

    that for itself is not very helpful until you take a look at the "Class attribute lookup" procedure when using metaclasses (I found a nice image of this in this blog). Because in case that an attribute raises an AttributeError and that class has a metaclass python looks at the metaclass.__getattr__ method and sees if that can resolve the attribute. To illustrate this with a minimal example:

    from types import DynamicClassAttribute
    
    # Metaclass
    class Funny(type):
    
        def __getattr__(self, value):
            print('search in meta')
            # Normally you would implement here some ifs/elifs or a lookup in a dictionary
            # but I'll just return the attribute
            return Funny.dynprop
    
        # Metaclasses dynprop:
        dynprop = 'Meta'
    
    class Fun(metaclass=Funny):
        def __init__(self, value):
            self._dynprop = value
    
        @DynamicClassAttribute
        def dynprop(self):
            return self._dynprop
    

    And here comes the "dynamic" part. If you call the dynprop on the class it will search in the meta and return the meta's dynprop:

    Fun.dynprop
    

    which prints:

    search in meta
    'Meta'
    

    So we invoked the metaclass.__getattr__ and returned the original attribute (which was defined with the same name as the new property).

    While for instances the dynprop of the Fun-instance is returned:

    Fun('Not-Meta').dynprop
    

    we get the overriden attribute:

    'Not-Meta'
    

    My conclusion from this is, that DynamicClassAttribute is important if you want to allow subclasses to have an attribute with the same name as used in the metaclass. You'll shadow it on instances but it's still accessible if you call it on the class.

    I did go into the behaviour of Enum in the old version so I left it in here:

    Old Version

    The DynamicClassAttribute is just useful (I'm not really sure on that point) if you suspect there could be naming conflicts between an attribute that is set on a subclass and a property on the base-class.

    You'll need to know at least some basics about metaclasses, because this will not work without using metaclasses (a nice explanation on how class attributes are called can be found in this blog post) because the attribute lookup is slightly different with metaclasses.

    Suppose you have:

    class Funny(type):
        dynprop = 'Very important meta attribute, do not override'
    
    class Fun(metaclass=Funny):
        def __init__(self, value):
            self._stub = value
    
        @property
        def dynprop(self):
            return 'Haha, overridden it with {}'.format(self._stub)
    

    and then call:

    Fun.dynprop
    

    property at 0x1b3d9fd19a8

    and on the instance we get:

    Fun(2).dynprop
    

    'Haha, overridden it with 2'

    bad ... it's lost. But wait we can use the metaclass special lookup: Let's implement an __getattr__ (fallback) and implement the dynprop as DynamicClassAttribute. Because according to it's documentation that's its purpose - to fallback to the __getattr__ if it's called on the class:

    from types import DynamicClassAttribute
    
    class Funny(type):
        def __getattr__(self, value):
            print('search in meta')
            return Funny.dynprop
    
        dynprop = 'Meta'
    
    class Fun(metaclass=Funny):
        def __init__(self, value):
            self._dynprop = value
    
        @DynamicClassAttribute
        def dynprop(self):
            return self._dynprop
    

    now we access the class-attribute:

    Fun.dynprop
    

    which prints:

    search in meta
    'Meta'
    

    So we invoked the metaclass.__getattr__ and returned the original attribute (which was defined with the same name as the new property).

    And for instances:

    Fun('Not-Meta').dynprop
    

    we get the overriden attribute:

    'Not-Meta'
    

    Well that's not too bad considering we can reroute using metaclasses to previously defined but overriden attributes without creating an instance. This example is the opposite that is done with Enum, where you define attributes on the subclass:

    from enum import Enum
    
    class Fun(Enum):
        name = 'me'
        age = 28
        hair = 'brown'
    

    and want to access these afterwards defined attributes by default.

    Fun.name
    # 
    

    but you also want to allow accessing the name attribute that was defined as DynamicClassAttribute (which returns which name the variable actually has):

    Fun('me').name
    # 'name'
    

    because otherwise how could you access the name of 28?

    Fun.hair.age
    # 
    # BUT:
    Fun.hair.name
    # returns 'hair'
    

    See the difference? Why does the second one don't return ? That's because of this use of DynamicClassAttribute. So you can shadow the original property but "release" it again later. This behaviour is the reverse of that shown in my example and requires at least the usage of __new__ and __prepare__. But for that you need to know how that exactly works and is explained in a lot of blogs and stackoverflow-answers that can explain it much better than I can so I'll skip going into that much depth (and I'm not sure if I could solve it in short order).

    Actual use-cases might be sparse but given time one can propably think of some...

    Very nice discussion on the documentation of DynamicClassAttribute: "we added it because we needed it"

提交回复
热议问题