【问题标题】:How to create a type that is closed under inherited operations?如何创建在继承操作下关闭的类型?
【发布时间】:2019-02-19 19:27:16
【问题描述】:

在数学意义上,如果操作总是返回集合本身的成员,则集合(或类型)在操作下是 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

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

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

【问题讨论】:

  • 所以,这有点吹毛求疵,但class BasicIntContainer(int) 不是一个 int 容器,它是一个 int
  • @juanpa.arrivillaga 我的术语可能有点不对劲,你怎么称呼这样的结构?
  • 它只是继承,用 OOP 的说法是一种“is-a”关系。如果将鼠标悬停在容器标签上,您会看到:“容器是一个类、数据结构或抽象数据类型,其实例是其他对象的集合。”。所以就像listdict。这里,BasicIntContainer 实例不是其他对象的集合,它是一种特殊的int 对象。我不想用更多的术语吹毛求疵来转移一个有趣的问题。无论如何,我只想改写为“如何创建在继承操作下关闭的类型”
  • 如果您只想将返回值转换为类型本身,您可以轻松编写一个简单的装饰器,但这样做的问题是可能有一些方法无法返回值如果是这种情况,则强制转换为正确的类型;你需要以某种方式区分它们,否则你可以用装饰器装饰类来装饰每个方法。
  • @IşıkKaplan 为了简单起见,我们假设__new__ 知道如何从它的所有超类中转换。此外,这个装饰器解决方案基本上是我用元类实现的。我目前正在修复它,但它变得非常复杂,我肯定会忘记很多角落。

标签: python python-3.x oop containers metaclass


【解决方案1】:

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

为此,您必须在课堂上重新定义 __getattribute____getattr__,并注意以下几点:

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

相同的基本转换包装器可用于所有属性和方法返回值。它只需要在 __getattribute____getattr__ 的结果上调用时恰好递归一次。

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

导入功能工具 def isdunder(x): 返回 isinstance(x, str) 和 x.startswith('__') 和 x.endswith('__') DunderSet 类: def __contains__(self, x): 返回 isdunder(x) def wrap_method(方法,xtype,演员): @functools.wraps(方法) def retval(*args, **kwargs): 结果 = 方法(*args,**kwargs) return cast(result) if type(result) == xtype else result 返回值 def wrap_getter(方法、xtype、cast、异常): @functools.wraps(方法) def retval(self, name, *args, **kwargs): 结果 = 方法(自我,名称,*args,**kwargs) 如果 name 在异常中返回结果 else check_type(result, xtype, cast) 返回值 def check_type(值,xtype,演员): 如果类型(值)== xtype: 返回演员表(值) 如果可调用(值): 返回 wrap_method(值,xtype,演员表) 返回值 类 ClosedMeta(类型): def __new__(meta, name, bases, 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__(meta, name, bases, dct, **kwargs) 对于 cls.__mro__ 中的基础: 对于名称,base.__dict__.items() 中的项目: 如果 isdunder(name) and (base is cls or name not in dct) and callable(item): 如果名称在('__getattribute__','__getattr__'): setattr(cls, name, wrap_getter(item, target, cls, exceptions)) elif 名称不在例外中: setattr(cls, name, wrap_method(item, target, cls)) 返回 cls def __init__(cls, *args, **kwargs): 返回 super().__init__(*args) 类 MyInt(int): def __contains__(self, x): 返回 x == 自我 def my_op(自我,其他): return int(self * self // 其他) 类 ClosedInt(MyInt,metaclass=ClosedMeta,目标=int, 例外=['__index__', '__int__', '__trunc__', '__hash__']): 经过 MyClass 类(ClosedInt,metaclass=type): def __add__(self, other): 返回 1 打印(类型(MyInt(1)+ MyInt(2))) 打印(MyInt(0)中的 0,MyInt(0)中的 1) 打印(类型(MyInt(4).my_op(16))) 打印(类型(ClosedInt(1)+ ClosedInt(2))) 打印(在 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'>

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

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

附录 1:改进的默认异常

我认为,通过仔细查看文档的special method names 部分,可以遵守比所有 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 的变量中,则可以完全删除类DunderSet,并且提取exceptions 的条件可以替换为:

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 in target(或blah in xtype)替换每次出现的blah == target(或包装器中的blah == xtype)。

【讨论】:

  • 我真的很好奇反对票。我错过了一些编码错误吗?毕竟我确实是在手机上写的。也许是太晚了,我不小心吐了一些废话?
  • 这对魔术方法没有任何作用,并且有一堆小错误,如collections.frozensetmap(string, ...)
  • @user2357112。我已经解决了这个问题以及其他一些问题。感谢您的反馈
  • @MadPhysicist 我真的很喜欢使用类声明 kwargs 的方法如何使异常成为显式的强制转换。这是一个精心设计的解决方案,干得好
  • @OlivierMelançon。谢谢你。这是我脑海中闪过一段时间的想法。感谢您给予我思考并写下来的动力。
【解决方案2】:

我认为使用一个类装饰器和一个不应该返回相同类型对象的方法的黑名单会有点 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

【讨论】:

    【解决方案3】:

    我仍然觉得可能有一种更自然的方式来实现这一点,但我能够解决问题中提供的尝试。

    以下是需要修复的要点。

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

    • __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 方法并标记已实施规则的例外情况,但除非某处有这些例外情况的列表,否则这似乎是不可避免的。

    【讨论】:

    • 可以使用类本身作为key,不需要uid
    • 但是类还没有创建
    【解决方案4】:

    每个人都在写短代码和元类,而我几乎不写装饰器。 (该死的,大声笑)但无论如何我都会分享它。

    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】:

      这是做不到的,数据模型禁止这样做。我可以向你证明:

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

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

      【讨论】:

      • 这很好,因为根据定义,闭包意味着如果xy 的类型为T,那么x + y 的类型为T。这里MyClass() 不是ClosedInt 类型。所以这个问题只有在操作的两个元素都是 ClosedInt 类型时才真正适用
      • 也做不到,一般情况下,原因是一样的——子类可以覆盖父类的任何行为。
      • @wim。看我的尝试。它并不完美,当然你可以通过多种方式绕过它,但我认为如果你想玩的话,它会起作用。
      • 我确信有办法让它几乎工作(顺便说一句,我不是你的答案的反对者)。我只是想表明任何尝试都可以被击败,所以在一般情况下这是不可能的。
      • 我以为我做了一个聪明的解决方法,但不是。我已将您的示例添加到我的答案中。直到你可以像那样覆盖元类。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2022-01-06
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-08-19
      • 2023-03-03
      相关资源
      最近更新 更多