如何创建在继承操作下关闭的类型?

Oli*_*çon 6 python oop containers metaclass python-3.x

在数学意义上,如果操作总是返回集合本身的成员,则在操作下关闭集合(或类型).

这个问题是关于在从超类继承的所有操作下关闭一个类.

考虑以下课程.

class MyInt(int):
    pass
Run Code Online (Sandbox Code Playgroud)

由于__add__没有被覆盖,因此不会因此而关闭.

x = MyInt(6)
print(type(x + x))  # <class 'int'>
Run Code Online (Sandbox Code Playgroud)

使类型关闭的一种非常繁琐的方法是手动转换返回intto 的每个操作的结果MyInt.

在这里,我使用元类自动化该过程,但这似乎是一个过于复杂的解决方案.

import functools

class ClosedMeta(type):
    _register = {}

    def __new__(cls, name, bases, namespace):
        # A unique id for the class
        uid = max(cls._register) + 1 if cls._register else 0

        def tail_cast(f):
            @functools.wraps(f)
            def wrapper(*args, **kwargs):
                out = f(*args, **kwargs)
                if type(out) in bases:
                    # Since the class does not exist yet, we will recover it later
                    return cls._register[uid](out)
                return out
            return wrapper

        for base in reversed(bases):
            for name, attr in base.__dict__.items():
                if callable(attr) and name not in namespace:
                    namespace[name] = tail_cast(attr)

        subcls = super().__new__(cls, name, bases, namespace)
        cls._register[uid] = subcls
        return subcls

class ClosedInt(int, metaclass=ClosedMeta):
    pass
Run Code Online (Sandbox Code Playgroud)

这在一些角落例如property和通过恢复的方法上失败了__getattribute__.当基数不仅仅由基类型组成时,它也会失败.

例如,这失败了:

class MyInt(int):
    pass

class ClosedInt(MyInt, metaclass=ClosedMeta):
    pass

ClosedInt(1) + ClosedInt(1) # returns the int 2
Run Code Online (Sandbox Code Playgroud)

我试图解决这个问题,但它似乎在兔子洞里越走越深.

这似乎是一个可能有一些简单的pythonic解决方案的问题.什么是其他更简洁的方式来实现这种封闭式?

Mad*_*ist 1

我认为使用元类的想法是可行的方法。诀窍是在获取值时动态地转换它们,而不是预先进行转换。这基本上就是 python 的全部内容:在你真正得到它之前,不知道你会得到什么或者那里有什么。

为此,您必须在您的班级上重新定义__getattribute__和 ,但需要注意一些注意事项:__getattr__

  1. 操作员不使用正常的属性访问方法。即使在元类上定义正确的__getattribute__and__getattr__也无济于事。必须为每个类显式覆盖 Dunders。
  2. __getattribute__和返回的方法__getattr__需要将其返回值转换为目标类型。这同样适用于称为操作员的 dunder。
  3. 有些方法应从#2 中排除,以确保机器正常运行。

相同的基本转换包装器可用于所有属性和方法返回值。它只需要在对__getattribute__or的结果调用时精确地递归一次__getattr__

下面显示的解决方案正是这样做的。它明确包装了未列为例外的所有 dunder。所有其他属性要么立即转换,要么包装(如果它们是函数)。__mro__它允许通过检查 中的所有内容(包括类本身)来自定义任何方法。该解决方案将与类和静态方法一起正常工作,因为它存储转换例程并且不依赖type(self)(正如我之前的一些尝试所做的那样)。它将正确排除 中列出的任何属性exceptions,而不仅仅是 dunder 方法。

导入函数工具


def isdunder(x):
    返回 isinstance(x, str) 和 x.startswith('__') 和 x.endswith('__')


邓德集类:
    def __contains__(self, x):
        返回 isdunder(x)


def wrap_method(方法,xtype,强制转换):

    @functools.wraps(方法)
    def retval(*args, **kwargs):
        结果 = 方法(*args, **kwargs)
        如果类型(结果)== xtype,则返回强制转换(结果),否则返回结果

    返回值


def wrap_getter(方法、xtype、强制转换、异常):
    @functools.wraps(方法)
    def retval(self, 名称, *args, **kwargs):
        结果 = 方法(自身,名称,*args,**kwargs)
        如果名称在异常中,则返回结果,否则 check_type(result, xtype,cast)

    返回值


def check_type(值, xtype, 强制转换):
    如果类型(值)== xtype:
        返回转换(值)
    如果可调用(值):
        返回wrap_method(值,xtype,强制转换)
    返回值


类 ClosedMeta(类型):
    def __new__(元、名称、基础、dct、**kwargs):
        如果 kwargs 中存在“异常”:
            例外=设置([
                '__new__'、'__init__'、'__del__'、
                '__init_subclass__', '__instancecheck__', '__subclasscheck__',
                *map(str, kwargs.pop('例外'))
            ])
        别的:
            异常 = DunderSet()
        target = kwargs.pop('target', bases[0] if bases else object)

        cls = super().__new__(元、名称、基础、dct、**kwargs)

        对于 cls.__mro__ 中的基数:
            对于名称,base.__dict__.items() 中的项目:
                if isdunder(name) and (base is cls or name not in dct) and callable(item):
                    if name in ('__getattribute__', '__getattr__'):
                        setattr(cls,名称,wrap_getter(项目,目标,cls,异常))
                    elif 名称不例外:
                        setattr(cls,名称,wrap_method(项目,目标,cls))
        返回CLS

    def __init__(cls, *args, **kwargs):
        返回 super().__init__(*args)


类 MyInt(int):
    def __contains__(self, x):
        返回 x == 自身
    def my_op(自己,其他):
        return int(自我 * 自我 // 其他)


类 ClosedInt(MyInt, 元类=ClosedMeta, 目标=int,
                异常=['__index__','__int__','__trunc__','__hash__']):
    经过

类 MyClass(ClosedInt, 元类=类型):
    def __add__(自己,其他):
        返回1

打印(类型(MyInt(1)+ MyInt(2)))
print(0 in MyInt(0), 1 in MyInt(0))
打印(类型(MyInt(4).my_op(16)))

打印(类型(ClosedInt(1)+ ClosedInt(2)))
print(ClosedInt(0) 中为 0, ClosedInt(0) 中为 1)
打印(类型(ClosedInt(4)。my_op(16)))

打印(类型(MyClass(1)+ ClosedInt(2)))

结果是

<class 'int'>
True False
<class 'int'> 

<class '__main__.ClosedInt'>
True False
<class '__main__.ClosedInt'>

<class 'int'>
Run Code Online (Sandbox Code Playgroud)

最后一个例子是对@wim 的回答的致敬。这表明你必须愿意这样做才能让它发挥作用。

IDEOne 链接,因为我现在无法访问计算机:https://ideone.com/iTBFW3

附录 1:改进的默认异常

我认为通过仔细查看文档的特殊方法名称部分,可以得到比所有 dunder 方法更好的默认异常集。方法可以分为两大类:具有非常具体的返回类型的方法,使 python 机制工作,以及当它们返回您感兴趣的类型的实例时应检查和包装其输出的方法。还有第三类,即应该始终排除的方法,即使您忘记明确提及它们。

以下是始终例外的方法的列表:

  • __new__
  • __init__
  • __del__
  • __init_subclass__
  • __instancecheck__
  • __subclasscheck__

以下是默认情况下应排除的所有内容的列表:

  • __repr__
  • __str__
  • __bytes__
  • __format__
  • __lt__
  • __le__
  • __eq__
  • __ne__
  • __gt__
  • __ge__
  • __hash__
  • __bool__
  • __setattr__
  • __delattr__
  • __dir__
  • __set__
  • __delete__
  • __set_name__
  • __slots__(不是方法,但仍然)
  • __len__
  • __length_hint__
  • __setitem__
  • __delitem__
  • __iter__
  • __reversed__
  • __contains__
  • __complex__
  • __int__
  • __float__
  • __index__
  • __enter__
  • __exit__
  • __await__
  • __aiter__
  • __anext__
  • __aenter__
  • __aexit__

如果我们将此列表存储到名为 的变量中,则可以完全删除default_exceptions该类,并且提取的条件可以替换为:DunderSetexceptions

exceptions = set([
    '__new__', '__init__', '__del__',
    '__init_subclass__', '__instancecheck__', '__subclasscheck__',
    *map(str, kwargs.pop('exceptions', default_exceptions))
])
Run Code Online (Sandbox Code Playgroud)

附录 2:改进的定位

应该可以很容易地定位多种类型。这在扩展 的其他实例时特别有用ClosedMeta,因为它可能不会覆盖我们想要的所有方法。

执行此操作的第一步是创建target类容器而不是单个类引用。代替

target = kwargs.pop('target', bases[0] if bases else object)
Run Code Online (Sandbox Code Playgroud)

target = kwargs.pop('target', bases[:1] if bases else [object])
try:
    target = set(target)
except TypeError:
    target = {target}
Run Code Online (Sandbox Code Playgroud)

现在用(or )替换出现的blah == target(或blah == xtype包装器中的) 。blah in targetblah in xtype

  • @OlivierMelançon。谢谢。这个想法在我脑海中盘旋了一段时间。我很感激你给了我思考并把它写下来的动力。 (2认同)