Welcome, guest | Sign In | My Account | Store | Cart

Using named Descriptors? Tired of duplicating the name of the instance in a string? A small metaclass can solve this.

Python, 54 lines
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
class NamedDescriptor(object):
    def __init__(self):
        # Name of the attribute, will be set by the classes with
        # the meta class DescriptorNameGetterMetaClass
        self.name = None

    def __get__(self, obj, type=None):
        if obj:
            # at the moment, we return the name of the descriptor, 
            # you can change this line to adapt to your needs
            return self.name
        else:
            return self

    def __set__(self, adaptor, value):
        if adaptor:
            # change the name, you can change this line to adapt to your needs
            self.name = value
        else:
            raise AttributeError('Cannot set this value')


class NamedDescriptorResolverMetaClass(type):
    '''
    This is where the magic happens.
    '''
    def __new__(cls, classname, bases, classDict):
        # Iterate through the new class' __dict__ and update all recognised NamedDescriptor member names
        for name, attr in classDict.iteritems():
            if isinstance(attr, NamedDescriptor):
                attr.name = name
        return type.__new__(cls, classname, bases, classDict)

    
class DescriptorHost(object):
    '''
    The Descriptor gets its name automatically even before any instance of the class is created,
    thanks MetaClass!
    >>> DescriptorHost.myFunDescriptor.name
    'myFunDescriptor'

    In our case, the NamedDescriptor returns it's name when being looked for from the host instance:
    >>> dh = DescriptorHost()
    >>> dh.myFunDescriptor
    'myFunDescriptor'
    '''
    __metaclass__ = NamedDescriptorResolverMetaClass

    myFunDescriptor = NamedDescriptor()


if __name__ == "__main__":
    import doctest
    doctest.testmod()

In classical examples of Descriptor use, a "name" argument need to be passed to the constructor of the Descriptor:

class DescriptorHost(object):

    myClassicDescriptor = NamedDescriptor(name='myClassicDescriptor')

This is redundant and increase the risk of typos. That's why I wanted to find a better way, and Python always has better ways!

Another possible implementation is to overload the __new__ method on the DescriptorHost class, but the main limitation is that the Descriptor will only receive it's name when an instance of DescriptorHost is created, which is a bit late.

This trick can also be used for collection of string constants, were every constant name is equal to its value:

class Cns(object):
    my_cns_1 = 'my_cns_1'
    my_cns_2 = 'my_cns_2'

could be replaced by:

class Cns(object):
    __metaclass__ = NamedDescriptorResolverMetaClass

    my_cns_1 = NamedDescriptor()
    my_cns_2 = NamedDescriptor()

1 comment

GLRenderer 4 years, 9 months ago  # | flag

There's an issue with this recipe. When setting a value to the NamedDescriptor it overwrites the value saved in 'name', it __set__ should be setting the value into another label instead of 'name'.

These are some corrections to the code (Python3):

```Python

class NamedDescriptor(object): """ A simple descriptor class to hold the label of the descriptor instance and the value assigned to it. """

def __init__(self):
    # Initialize the holder for name and value attributes.
    # These two values will be set later by the metaclass 
    # NamedDescriptorResolverMetaClass
    self.name = None
    self.value = None

def __get__(self, instance, owner=None):
    if instance is None:
        return self

    print("Retrieving {name}".format(name=self.name))
    return self.value

def __set__(self, adaptor, value):
    if adaptor is None:
        raise AttributeError('Cannot set this value')

    # change the name, you can change this line to adapt to your needs
    print("Accessing {name} = {value}".format(name=self.name, value=value))
    self.value = value

class NamedDescriptorResolverMetaClass(type): ''' This is where the magic happens. The type class is the first one receiving any arguments in the construction of a class. The __new__ method intercepts at creation time. Here is where we look for the name of the attribute. The descriptors are used in conjunction with this metaclass, otherwise it wouldn't be possible to obtain the attribute name. ''' def __new__(cls, classname, bases, classDict): # Iterate through the new class' __dict__ and update all recognised NamedDescriptor member names for name, attr in classDict.items(): if isinstance(attr, NamedDescriptor): # This calls the method __set__ in the NamedDescriptor # It's equivalent to # type(NamedDescriptor).__dict__['attr'].__set__('attr', 'name') print("attr: {}, name: {}".format(attr, name)) attr.name = name return type.__new__(cls, classname, bases, classDict)

class DescriptorHost(object, metaclass=NamedDescriptorResolverMetaClass): ''' The Descriptor gets its name automatically even before any instance of the class is created, thanks MetaClass!

DescriptorHost.myFunDescriptor.name 'myFunDescriptor'

In our case, the NamedDescriptor returns it's name when being looked for from the host instance:
>>> dh = DescriptorHost()
>>> dh.myFunDescriptor
Retrieving myFunDescriptor
'''
myFunDescriptor = NamedDescriptor()

if __name__ == "__main__": import doctest doctest.testmod()

```