Reputation: 673
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
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
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