pd.DataFrame(...) 在元类之前定义时导致 TypeError

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

我一直在研究 metaclasses 以尝试对它们有一个好的感觉。我想出的一个非常简单(而且毫无意义)的方法如下:

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

class 单独运行时效果很好。

现在当我做类似的事情时:

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]}))

我收到以下错误:

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

奇怪的是(至少对我而言)如果我在 metaclass 的定义之前放置另一个 print(pd.DataFrame({'x': [1, 2]})),整个事情就可以了。出于某种原因,它必须是印刷品...

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]}))

所以作为一个 hacky 解决方案,我绝对可以使用它...

此外,当我将 collections.abc.Mapping 作为 MappingMeta 的超级 class 删除时,我没有收到错误 - 但是我没有得到我正在寻找的功能(本质上是使用 Test 作为字典)

我知道这可能不是 metaclasses 的最佳用途,但我很好奇是否有人知道发生了什么。

编辑

已接受的回复回答了问题,但只是为了提供一些关于 为什么 的上下文,我在 meta[ 中使用了 collections.abc.Mapping class =48=]。我这样写的原因是我可以像下面这样写 classes:

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}

虽然接受的答案肯定回答了这个问题,但我最终决定只实施 collections.abc.Mapping 提供的方法以避免任何其他潜在的冲突

我意识到这可能会造成比它解决的更多的麻烦,但如果您不打算从 MappingMeta 派生任何东西,这似乎可以解决问题(因为您上面的代码运行)。

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

问题在于 collections.abc 中的抽象基础 class 而不是 旨在用作元 classes。

虽然这可能是“可以想象的”,但需要确切地知道他在做什么:metaclasses 用于创建模板classes 本身,而 collections.abc 是基础 classes - 不是 metaclasses,提供了一个框架来实现通用集合模式工作量最少。

所以,发生的事情显然是在实例化 collections.abc.mapping 的实例时,Python 机器对所有已注册的映射类型进行了一些检查,而你的 quimera 妨碍了一切,并使事情休息。

干净的解决方案就是在您的 metaclass 上手动实现您想要的任何映射方法。即使你开始解决这个问题——就像@DS_London 的答案一样,Mapping mixin 实现了很多可能与 type 本身需要的机制冲突的方法和机制——而且你有那时无法控制。你得到的错误就是这样一个例子。

collections.abc 将免费提供给您的只是 __contains__, keys, items, values, get, __eq__, __ne__ - 您甚至可能不需要所有这些 - 所以只需实施您想要的任何东西,然后完成。

如果在定义元 class 之前实例化,Dataframe 不会中断的原因很简单:在此之前,collections.abc 虚拟子 class 的机制] 检查还没有被你的 metaclass.

毒化