【问题标题】:pd.DataFrame(...) resulting in TypeError when a metaclass is defined before itpd.DataFrame(...) 在它之前定义元类时导致 TypeError
【发布时间】:2021-08-27 04:11:32
【问题描述】:

我一直在玩元类来尝试对它们有一个好的感觉。我想出的一个非常简单(且毫无意义)的方法如下:

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

该类在隔离时完美运行。

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

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

奇怪的是(至少对我来说)如果我在元类的定义之前加上另一个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 的超类删除时,我没有收到错误消息 - 但是我没有得到我正在寻找的功能(基本上使用 Test 作为字典)

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

编辑

接受的响应回答了这个问题,但只是为了提供一些关于为什么我在元类中使用 collections.abc.Mapping 类的上下文。我这样写的原因是我可以编写如下类:

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 提供的方法以避免任何其他潜在的冲突

【问题讨论】:

  • 可以复制这个。 Pandas 似乎在内部调用类型的 subclasses。如果在从 type 派生之前调用 print(),pandas 对此一无所知,因为 type 还没有 MappingMeta 作为子类。如果你调用 print 之后,type 现在有一个 MappingMeta 的子类。如果 pandas 递归调用 subclasses,也许您的 MappingMeta 类需要处理它?我对 Python 多重继承知之甚少,不知道遵循哪条通往 type.__subclasses__() 的路线:直接通过 type() 或通过 collections.abc.Mapping。也许第一个 panda 的调用正在缓存?

标签: python python-3.x pandas metaclass


【解决方案1】:

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

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

【讨论】:

    【解决方案2】:

    问题是collections.abc 中的抽象基类并非设计为用作元类。

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

    因此,显然在实例化collections.abc.mapping 的实例时,Python 机器会对所有已注册的映射类型进行一些检查,而您的 quimera 会妨碍您,让事情中断。

    干净的解决方案是手动在您的元类上实现您想要的任何映射方法。即使你解决了这个问题——就像@DS_London 的回答一样,Mapping mixin 实现了许多可能与type 本身所需要的机制发生冲突的方法和机制——你无法控制。你得到的错误就是这样一个例子。

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

    如果在定义元类之前实例化Dataframe 不会中断,原因很简单:在此之前,用于虚拟子类检查的 collections.abc 机制还没有被元类毒害。

    【讨论】:

    • 感谢您的解释。我认为这将归结为“这不是一个好主意,所以最好离开它”。确实,我想使用collections.abc.Mapping 的唯一原因是免费获得这些方法,但现在我意识到最好少用魔法,自己实现这些方法!
    • @user3645016 所以,从某种意义上说,您的代码提前中断是件好事。否则,您可能会在以后建立大量阴险的陷阱! FWIW 我过去常常从 C++ 标准容器(也应该继承)愉快地进行多重继承,直到我被带到这里的任务...
    • 是的 100%...不过,这并不是关键任务,谢天谢地只是一些孤立的实验。对此的任何代码审查都有望以:“只需使用该死的字典和 K.I.S.S.!”
    猜你喜欢
    • 2011-02-23
    • 1970-01-01
    • 2019-10-13
    • 1970-01-01
    • 1970-01-01
    • 2021-06-16
    • 1970-01-01
    • 1970-01-01
    • 2023-03-11
    相关资源
    最近更新 更多