user3645016
user3645016

Reputation: 673

pd.DataFrame(...) resulting in TypeError when a metaclass is defined before it

I've been playing around with metaclasses to try and get a good feel of them. A really simple (and pointless) one I came up with is the following:

class MappingMeta(type, collections.abc.Mapping):
    def __setattr__(self, *args, **kwargs):
        raise RuntimeError("Can not set attributes of Mapping type")

    def __call__(self, *args, **kwargs):
        raise RuntimeError("Can not directly instantiate Mapping type")

    def __getitem__(self, value):
        return getattr(self, value)

    def __iter__(self):
        return (k for k in vars(self) if not k.startswith("_"))

    def __len__(self):
        return sum(1 for _ in self)


class Mapping(metaclass=MappingMeta):
    pass


class Test(Mapping):
    x = 1
    y = 2

The class works perfectly in isolation.

Now when I do something like:

import pandas as pd
class MappingMeta(type, collections.abc.Mapping):
    ... # same as above

class Mapping(metaclass=MappingMeta):
    pass


class Test(Mapping):
    x = 1
    y = 2

print(pd.DataFrame({'x': [1, 2]}))

I get the following error:

Traceback (most recent call last):
  File "metamapping.py", line 22, in <module>
    print(pd.DataFrame({"x": [1, 2]}))
  File "/Users/rnlondono/miniconda3/envs/p38/lib/python3.8/site-packages/pandas/core/frame.py", line 803, in __repr__
    self.to_string(
  File "/Users/rnlondono/miniconda3/envs/p38/lib/python3.8/site-packages/pandas/core/frame.py", line 939, in to_string
    return fmt.DataFrameRenderer(formatter).to_string(
  File "/Users/rnlondono/miniconda3/envs/p38/lib/python3.8/site-packages/pandas/io/formats/format.py", line 1031, in to_string
    string = string_formatter.to_string()
  File "/Users/rnlondono/miniconda3/envs/p38/lib/python3.8/site-packages/pandas/io/formats/string.py", line 23, in to_string
    text = self._get_string_representation()
  File "/Users/rnlondono/miniconda3/envs/p38/lib/python3.8/site-packages/pandas/io/formats/string.py", line 38, in _get_string_representation
    strcols = self._get_strcols()
  File "/Users/rnlondono/miniconda3/envs/p38/lib/python3.8/site-packages/pandas/io/formats/string.py", line 29, in _get_strcols
    strcols = self.fmt.get_strcols()
  File "/Users/rnlondono/miniconda3/envs/p38/lib/python3.8/site-packages/pandas/io/formats/format.py", line 519, in get_strcols
    strcols = self._get_strcols_without_index()
  File "/Users/rnlondono/miniconda3/envs/p38/lib/python3.8/site-packages/pandas/io/formats/format.py", line 752, in _get_strcols_without_index
    if not is_list_like(self.header) and not self.header:
  File "pandas/_libs/lib.pyx", line 1033, in pandas._libs.lib.is_list_like
  File "pandas/_libs/lib.pyx", line 1038, in pandas._libs.lib.c_is_list_like
  File "/Users/rnlondono/miniconda3/envs/p38/lib/python3.8/abc.py", line 98, in __instancecheck__
    return _abc_instancecheck(cls, instance)
  File "/Users/rnlondono/miniconda3/envs/p38/lib/python3.8/abc.py", line 102, in __subclasscheck__
    return _abc_subclasscheck(cls, subclass)
  File "/Users/rnlondono/miniconda3/envs/p38/lib/python3.8/abc.py", line 102, in __subclasscheck__
    return _abc_subclasscheck(cls, subclass)
  File "/Users/rnlondono/miniconda3/envs/p38/lib/python3.8/abc.py", line 102, in __subclasscheck__
    return _abc_subclasscheck(cls, subclass)
  [Previous line repeated 1 more time]
TypeError: descriptor '__subclasses__' of 'type' object needs an argument

The strange thing is (at least for me) if I put another print(pd.DataFrame({'x': [1, 2]})) before the definition of the metaclass, the whole thing works. For some reason, it has to be a print...

import pandas as pd
print(pd.DataFrame({'x': [1, 2]}))

class MappingMeta(type, collections.abc.Mapping):
    ... # same as above

class Mapping(metaclass=MappingMeta):
    pass


class Test(Mapping):
    x = 1
    y = 2

print(pd.DataFrame({'x': [1, 2]}))

So as a hacky solution I could definitely go with that...

Also when I remove the collections.abc.Mapping as a superclass of MappingMeta I do not get the error - but then I do not get the functionality that I'm looking for (essentially using Test as a dictionary)

I'm aware this is probably not the best use of metaclasses but I'm just curious if anyone has any idea on what is going on.

EDIT

Accepted response answers the question, but just to provide a bit of context as to why I used the collections.abc.Mapping class inside of a metaclass. The reason I wrote this like this was so that I could write classes like the following:

class Test(Mapping): 
    x = 1
    y = 2
    z = 3

'x' in Test           # True
list(Test.items())    # [('x', 1), ('y', 2), ('z', 3)]
{**Test}              # {'x': 1, 'y': 2, 'z': 3}

Though the accepted answer definitely answers the question, I decided ultimately to just implement the methods provided by collections.abc.Mapping to avoid any other potential conflicts

Upvotes: 1

Views: 78

Answers (2)

jsbueno
jsbueno

Reputation: 110208

The thing is that the abstract base class in collections.abc are not designed to be used as metaclasses.

Although it could be "thinkable" about, one would need to know exactly what is it he is doing: metaclasses are for templating the creation of classes themselves, and collections.abc are base classes - not metaclasses, providing a framework to implement common collection patterns with minimal amount of work.

So, what takes place is that apparently when instantiating an instance of collections.abc.mapping, Python machinery does some checking on all registered mapping types, and your quimera gets in the way, and make things break.

The clean solution is just implement whatever mapping methods you want on yur metaclass manually. Even if you get to work around this issue - like the answer by @DS_London does, the Mapping mixin implements a lot of methods and mechanisms that might conflict with the mechanisms neded by type itself - and you have no control over then. The error you got is one such example.

What collections.abc would give you for free are just __contains__, keys, items, values, get, __eq__, __ne__ - and you might not even need all of those - so just implement whatever you want, and get done.

And the reason Dataframe does not break if instantiated before you define your metaclass is quite simple: before that, the collections.abc machinery for virtual-subclass checking is not yet poisoned by your metaclass.

Upvotes: 2

DS_London
DS_London

Reputation: 4261

I realize this may create more trouble than it solves, but if you don't plan to derive anything from MappingMeta, this seems to get around the problem (in that your code above runs).

class MappingMeta(type,collections.abc.Mapping):
    def __subclasses__(obj=None):
        return []

Upvotes: 1

Related Questions