代码之家  ›  专栏  ›  技术社区  ›  Olivier Melançon iacob

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

  •  6
  • Olivier Melançon iacob  · 技术社区  · 6 年前

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

    这个问题是关于使一个类在继承自其超类的所有操作下都是封闭的。

    class MyInt(int):
        pass
    

    __add__ 未被覆盖,在“添加”下未关闭。

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

    使类型关闭的一种非常繁琐的方法是手动强制转换每个返回 int 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
    

    这在某些情况下失败,例如 property 以及通过 __getattribute__ . 当基不只是由基类型组成时,它也会失败。

    class MyInt(int):
        pass
    
    class ClosedInt(MyInt, metaclass=ClosedMeta):
        pass
    
    ClosedInt(1) + ClosedInt(1) # returns the int 2
    

    我试图解决这个问题,但它似乎越来越深的兔子洞。

    这似乎是一个问题,可能有一些简单的蟒蛇解决方案。还有什么更整洁的方法可以实现这样一种封闭的类型?

    5 回复  |  直到 6 年前
        1
  •  1
  •   Mad Physicist    6 年前

    要做到这一点,你必须重新定义 __getattribute__ __getattr__ 在你的课堂上有一些注意事项:

    1. 运算符不执行常规的属性访问方法。甚至定义权利 在你的元类上也没用。必须为每个类显式重写Dunders。
    2. __获取属性__ 需要将其返回值强制转换为目标类型。这同样适用于被称为操作符的dunders。

    相同的基本转换包装器可以用于所有属性和方法返回值。它只需要在调用 __获取属性__ .

    下面显示的解决方案正是这样做的。它显式地包装所有未列为例外的dunder。所有其他属性要么立即强制转换,要么包装(如果它们是函数)。它允许通过检查 __mro__ ,包括类本身。该解决方案将与类和静态方法一起正确工作,因为它存储转换例程,并且不依赖于 type(self) exceptions ,而不仅仅是dunder方法。

    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'>
    

    最后一个例子是对 @wim's answer . 这说明你必须 为了让它工作。

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

    附录1:改进的违约例外

    special method names 文件的一节。方法可以分为两大类:一类是具有使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 ,班级 DunderSet 例外情况 可替换为:

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

    附录2:改进的目标

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

    target 而不是单个类引用。而不是

    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 == target (或 blah == xtype 在包装)与 blah in target (或 blah in xtype ).

        2
  •  2
  •   blhsing    6 年前

    我认为使用一个类修饰符和一个不应该返回相同类型对象的方法黑名单会更像python:

    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
    
        3
  •  1
  •   wim    6 年前

    这是无法做到的,数据模型禁止这样做。我可以证明给你看:

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

    加法首先由left hand对象处理,如果left类型处理它(即不返回 NotImplemented (辛格尔顿)那么 没有什么 关于 other __radd__ -当然,这在一般情况下是不可能的。

        4
  •  1
  •   Işık Kaplan    6 年前

    每个人都在写简短的代码和元类,而我几乎没有写装饰。(该死的,哈哈)但我还是要和你分享。

    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
    

    现在,在完成了这么长的设置之后,您只需像平常一样装饰类;我太困了,无法让它与继承的类一起工作,因此现在您必须装饰从封闭类继承的类。

    @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
    

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

        5
  •  1
  •   Olivier Melançon iacob    6 年前

    我还是觉得可能还有更多 这是一个很好的方法,但我能够修复问题中提供的尝试。

    • 我们必须检查 基地 ;

    • __getattribute__ __getattr__ 必须视为特殊情况处理;

    • 属性 __get__

    • 我们必须编写一个异常列表作为方法,例如 __int__ __eq__

    代码

    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方法以及实现规则的标记例外,但是除非在某个地方有这些方法的列表,否则这似乎是不可避免的。