Dealing with non-callable classmethod objects

Cameron Simpson cs at cskk.id.au
Fri Nov 11 17:47:57 EST 2022


On 11Nov2022 15:29, Ian Pilcher <arequipeno at gmail.com> wrote:
>I am trying to figure out a way to gracefully deal with uncallable
>classmethod objects.

I'm just going to trim your example below a bit for reference purposes:

>class DUID(object):
>    def __init__(self, d):
>        for attr, factory in self._attrs.items():
>            setattr(self, attr, factory(d[attr]))
>    @classmethod
>    def from_dict(cls, d):
>        subcls = cls._subclasses[d['duid_type']]
>        return subcls(d)
>
>class DuidLL(DUID):
>    @staticmethod
>    def _parse_l2addr(addr):
>        return bytes.fromhex(addr.replace(':', ''))
>    _attrs = { 'layer2_addr': _parse_l2addr }
>
>class DuidLLT(DuidLL):
>    @classmethod
>    def _parse_l2addr(cls, addr):
>        return super()._parse_l2addr(addr)
>    _attrs = {
>            'layer2_addr': _parse_l2addr,
>        }

So what you've got is that `for attr, factory in self._attrs.items():` 
loop, where the factory comes from the subclass `_attrs` mapping. For 
`DuidLL` you get the static method `_parse_l2addr` object and for 
`DuidLLT` you get the class method object.

[...]
>This works with static methods (as well as normal functions and object
>types that have an appropriate constructor): [...]
[...]
>
>It doesn't work with a class method, such as DuidLLT._parse_l2addr():
>
>>>>duid_llt = DUID.from_dict({ 'duid_type': 'DUID-LLT', 'layer2_addr': 'de:ad:be:ef:00:00', 'time': '2015-09-04T07:53:04-05:00' })
>Traceback (most recent call last):
>  File "<stdin>", line 1, in <module>
>  File "/home/pilcher/subservient/wtf/wtf.py", line 19, in from_dict
>    return subcls(d)
>  File "/home/pilcher/subservient/wtf/wtf.py", line 14, in __init__
>    setattr(self, attr, factory(d[attr]))
>TypeError: 'classmethod' object is not callable
>
>In searching, I've found a few articles that discuss the fact that
>classmethod objects aren't callable, but the situation actually seems to
>be more complicated.
>
>>>> type(DuidLLT._parse_l2addr)
><class 'method'>
>>>> callable(DuidLLT._parse_l2addr)
>True
>
>The method itself is callable, which makes sense.  The factory function
>doesn't access it directly, however, it gets it out of the _attrs
>dictionary.
>
>>>> type(DuidLLT._attrs['layer2_addr'])
><class 'classmethod'>
>>>> callable(DuidLLT._attrs['layer2_addr'])
>False
>
>I'm not 100% sure, but I believe that this is happening because the
>class (DuidLLT) doesn't exist at the time that its _attrs dictionary is
>defined.  Thus, there is no class to which the method can be bound at
>that time and the dictionary ends up containing the "unbound version."

Yes. When you define the dictionary `_parse_l2addr` is an unbound class 
method object. That doesn't change.

>Fortunately, I do know the class in the context from which I actually
>need to call the method, so I am able to call it with its __func__
>attribute.  A modified version of DUID.__init__() appears to work:
>
>    def __init__(self, d):
>        for attr, factory in self._attrs.items():
>            if callable(factory):  # <============= ???!
>                value = factory(d[attr])
>            else:
>                value = factory.__func__(type(self), d[attr])
>            setattr(self, attr, value)

Neat!

>A couple of questions (finally!):
>* Is my analysis of why this is happening correct?

It seems so to me. Although I only learned some of these nuances 
recently.

>* Can I improve the 'if callable(factory):' test above?  This treats
>  all non-callable objects as classmethods, which is obviously not
>  correct.  Ideally, I would check specifically for a classmethod, but
>  there doesn't seem to be any literal against which I could check the
>  factory's type.

Yeah, it does feel a bit touchy feely.

You could see if the `inspect` module tells you more precise things 
about the `factory`.

The other suggestion I have is to put the method name in `_attrs`; if 
that's a `str` you could special case it as a well known type for the 
factory and look it up with `getattr(cls,factory)`.

Cheers,
Cameron Simpson <cs at cskk.id.au>


More information about the Python-list mailing list