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

How to create a type that is closed under inherited operations?

在数学意义上,如果操作总是 returns 集合本身的成员,则集合(或类型)在操作下 closed

这个问题是关于制作一个class,它在从它的超classes继承的所有操作下关闭。

考虑以下 class。

class MyInt(int):
    pass

由于__add__没有被覆盖,所以在添加下不关闭

x = MyInt(6)
print(type(x + x))  # <class 'int'>

关闭类型的一种非常繁琐的方法是手动将 returns 和 int 的每个操作的结果强制转换为 MyInt.

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

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

这在某些极端情况下失败,例如 property 和通过 __getattribute__ 恢复的方法。当基不只由基类型组成时,它也会失败。

例如,这失败了:

class MyInt(int):
    pass

class ClosedInt(MyInt, metaclass=ClosedMeta):
    pass

ClosedInt(1) + ClosedInt(1) # returns the int 2

我试图解决这个问题,但它似乎越陷越深。

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

这不能做,数据模型禁止这样做。我可以证明给你看:

>>> class MyClass(ClosedInt, metaclass=type):
...     def __add__(self, other):
...         return 'potato'
...     
>>> MyClass(1) + ClosedInt(2)
'potato'

加法首先由左侧对象处理,如果左侧类型处理它(即不 return NotImplemented 单例),则 about other 在此操作中被考虑。如果右手类型是左手类型的子类,您可以使用反射方法控制结果__radd__ - 当然在一般情况下这是不可能的。

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

为此,您必须在 class 上重新定义 __getattribute____getattr__,但有一些注意事项:

  1. 运算符不通过正常的属性访问方法。即使在您的 metaclass 上定义正确的 __getattribute____getattr__ 也无济于事。必须为每个 class.
  2. 显式覆盖 Dunder
  3. __getattribute____getattr__ 编写的 return 方法需要将它们的 return 值转换为目标类型。这同样适用于称为运算符的 dunders。
  4. #2 中应排除一些方法以确保机器正常运行。

相同的基本铸造包装器可用于所有属性和方法 return 值。当调用 __getattribute____getattr__.

的结果时,它只需要恰好递归一次

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

import functools


def isdunder(x):
    return isinstance(x, str) and x.startswith('__') and x.endswith('__')


class DunderSet:
    def __contains__(self, x):
        return isdunder(x)


def wrap_method(method, xtype, cast):

    @functools.wraps(method)
    def retval(*args, **kwargs):
        result = method(*args, **kwargs)
        return cast(result) if type(result) == xtype else result

    return retval


def wrap_getter(method, xtype, cast, exceptions):
    @functools.wraps(method)
    def retval(self, name, *args, **kwargs):
        result = method(self, name, *args, **kwargs)
        return result if name in exceptions else check_type(result, xtype, cast)

    return retval


def check_type(value, xtype, cast):
    if type(value) == xtype:
        return cast(value)
    if callable(value):
        return wrap_method(value, xtype, cast)
    return value


class ClosedMeta(type):
    def __new__(meta, name, bases, dct, **kwargs):
        if 'exceptions' in kwargs:
            exceptions = set([
                '__new__', '__init__', '__del__',
                '__init_subclass__', '__instancecheck__', '__subclasscheck__',
                *map(str, kwargs.pop('exceptions'))
            ])
        else:
            exceptions = DunderSet()
        target = kwargs.pop('target', bases[0] if bases else object)

        cls = super().__new__(meta, name, bases, dct, **kwargs)

        for base in cls.__mro__:
            for name, item in 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, name, wrap_getter(item, target, cls, exceptions))
                    elif name not in exceptions:
                        setattr(cls, name, wrap_method(item, target, cls))
        return cls

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


class MyInt(int):
    def __contains__(self, x):
        return x == self
    def my_op(self, other):
        return int(self * self // other)


class ClosedInt(MyInt, metaclass=ClosedMeta, target=int,
                exceptions=['__index__', '__int__', '__trunc__', '__hash__']):
    pass

class MyClass(ClosedInt, metaclass=type):
    def __add__(self, other):
        return 1

print(type(MyInt(1) + MyInt(2)))
print(0 in MyInt(0), 1 in MyInt(0))
print(type(MyInt(4).my_op(16)))

print(type(ClosedInt(1) + ClosedInt(2)))
print(0 in ClosedInt(0), 1 in ClosedInt(0))
print(type(ClosedInt(4).my_op(16)))

print(type(MyClass(1) + ClosedInt(2)))

结果是

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

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

<class 'int'>

最后一个例子是对的致敬。它表明您必须想要这样做才能正常工作。

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

附录 1:改进的默认异常

我认为通过仔细阅读文档的 special method names 部分,可以得到比所有 dunder 方法更好的默认异常集。方法可以分为两大类 classes:具有使 python 机制工作的非常具体 return 类型的方法,以及在 return 您感兴趣的实例。还有第三类,即应始终排除在外的方法,即使您忘记明确提及它们也是如此。

以下是始终排除的方法列表:

  • __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 的变量中,则 class DunderSet 可以完全删除,并且提取 exceptions 的条件可以替换为:

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

附录 2:改进的定位

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

这样做的第一步是使 target 成为 class 的容器,而不是单个 class 引用。而不是

target = kwargs.pop('target', bases[0] if bases else object)

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

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

我仍然觉得可能有更自然的方法来完成这个,但我能够解决问题中提供的尝试。

以下是需要修复的要点。

  • 我们必须检查 mro 中所有 类 的方法,而不仅仅是 bases

  • __getattribute____getattr__必须作为特例处理;

  • 具有__get__的属性必须分开处理;

  • 我们必须编写异常列表,因为 __int____eq__ 等方法显然应该 return 它们的预期类型。

代码

import functools

def get_mro(bases):
    # We omit 'object' as it is the base type
    return type('', bases, {}).__mro__[1:-1]

class ClosedMeta(type):
    _register = {}

    # Some methods return type must not change
    _exceptions = ('__int__', '__eq__', ...)

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

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

        def deep_tail_cast(f):
            """Cast the return value of f or the return value of f(...)"""
            @functools.wraps(f)
            def wrapper(*args, **kwargs):
                out = f(*args, **kwargs)
                if callable(out):
                    return tail_cast(out)
                elif type(out) in mro:
                    return cls._register[uid](out)
                else:
                    return out
            return wrapper

        class PropertyCast:
            """Cast the return value of a property"""
            def __init__(self, prop):
                self.prop = prop

            def __get__(self, instance, owner):
                return cls._register[uid](self.prop.__get__(instance, owner))

            def __set__(self, instance, value):
                return self.prop.__set__(instance, value)

            def __delete__(self, instance):
                return self.prop.__delete__(instance)

        for base in reversed(mro):
            for name, attr in base.__dict__.items():
                if name in ('__getattr__', '__getattribute__'):
                    namespace[name] = deep_tail_cast(attr)
                elif callable(attr) and name not in namespace and name not in cls._exceptions:
                    namespace[name] = tail_cast(attr)
                elif hasattr(attr, '__get__'):
                    namespace[name] = PropertyCast(attr)

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

例子

class MyInt(int):
    def __getattr__(self, _):
        return 1

    @property
    def foo(self):
        return 2

class ClosedInt(MyInt, metaclass=ClosedMeta):
    pass

x = ClosedInt(2)
print(type(x * x), x * x)
print(type(x.foo), x.foo)
print(type(x.bar), x.bar)

输出

<class '__main__.ClosedIntContainer'> 4
<class '__main__.ClosedIntContainer'> 2
<class '__main__.ClosedIntContainer'> 1

这还有一些问题。举例来说,我们仍然有繁琐的任务来检查所有 dunder 方法并标记已实施规则的例外情况,但除非某处有这些列表,否则这似乎是不可避免的。

每个人都在写短代码和元 classes,而我几乎不写装饰器。 (该死的,哈哈)但我还是要分享它。

from functools import wraps


class CLOSED:
    _built_ins = [
        '__add__', '__sub__', '__mul__', '__floordiv__',
        '__div__', '__truediv__', '__mod__', '__divmod__',
        '__pow__', '__lshift__', '__rshift__','__and__',
        '__or__', '__xor__',
    ]

    @staticmethod
    def register_closed(method):  # Or you can use type annotations
        method.registered = True  # Or you can add the method names as string to closed decorator
        return method  # In this version you decorate the methods with this

    @staticmethod
    def closed_method(method, cls):
        @wraps(method)
        def wrapper(*a, **kw):
            return cls(method(*a, **kw))

        return wrapper

    @classmethod
    def closed_class(klass, cls):
        for magic in klass._built_ins:
            _method = getattr(cls, magic, False)
            if _method:
                setattr(cls, magic, klass.closed_method(_method, cls))

        for method in dir(cls):
            c1 = method not in klass._built_ins
            c2 = method not in dir(object)
            c3 = getattr(getattr(cls, method), 'registered', False)
            if all((c1, c2, c3)):
                _method = getattr(cls, method)
                setattr(cls, method, klass.closed_method(_method, cls))
        return cls

完成这么长的设置后,您只需像往常一样装饰 class;我太困了,无法让它与继承的 classes 一起工作,所以现在你必须装饰从封闭的 class.

继承的 class
@CLOSED.closed_class
class foo(int):
    @CLOSED.register_closed  # or if you can simply add this to CLOSED.closed_class
    def bar(self, other):    # if you are certain that every method can be casted to its own class
        """Basically just the __add__ method"""
        return self + other


print(type(foo(1) + foo(1))); print(foo(1) + foo(1))  # <class '__main__.foo'> 2
print(type(foo(1).bar(2))); print(foo(1).bar(2))      # <class '__main__.foo'> 3


@CLOSED.closed_class
class baz(foo):
    pass

print(type(baz(1) + baz(3))); print(baz(1) + baz(3))  # <class '__main__.baz'> 4
print(type(baz(1).bar(4))); print(baz(1).bar(4))      # <class '__main__.baz'> 5

请随意投反对票,因为我仍然不确定我是否正确理解了这个问题。

我认为使用 class 装饰器和不应 return 相同类型对象的方法黑名单会更像 Pythonic:

class containerize:
    def __call__(self, obj):
        if isinstance(obj, type):
            return self.decorate_class(obj)
        return self.decorate_callable(obj)

    def decorate_class(self, cls):
        for name in dir(cls):
            attr = getattr(cls, name)
            if callable(attr) and name not in ('__class__', '__init__', '__new__', '__str__', '__repr__', '__getattribute__'):
                setattr(cls, name, self.decorate_callable(attr))
        return cls

    def decorate_callable(self, func):
        def wrapper(obj, *args, **kwargs):
            return obj.__class__(func(obj, *args, **kwargs))
        return wrapper

这样:

class MyInt(int):
    pass

@containerize()
class ClosedIntContainer(MyInt):
    pass

i = ClosedIntContainer(3) + ClosedIntContainer(2)
print(i, type(i).__name__)

会输出:

5 ClosedIntContainer

作为奖励,装饰器也可以有选择地用于个别方法:

class MyInt(int):
    @containerize()
    def __add__(self, other):
        return super().__add__(other)

i = MyInt(3) + MyInt(2)
print(i, type(i).__name__)

这输出:

5 MyInt