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