【问题标题】:Case-specific mix-ins?特定于案例的混合?
【发布时间】:2021-05-30 11:21:01
【问题描述】:

python 中有没有一种方法可以根据其他参数继承不同的 mixin 类?示例:

a = MyClass(case='A')  # spawns a class instance which inherits MixinA
b = MyClass(case='B')  # spawns a class instance which inherits MixinB

MixinAMixinB 都需要访问MyClass 的数据,但本身不托管任何数据。它们都定义了不同版本的some_methodself.some_method() 将在MyClass 中引用它们,但是根据传递给MyClasscase-argument 执行不同的操作。

我不确定这是否有意义或是否有更好的设计。在我的应用程序中,性能是关键,因此在调用MixinAMixinB 定义的特定于案例的方法时,我想尽量避免开销。我在某处读到使用 mixins 会对此有好处。但也许有更好的方法。

【问题讨论】:

  • @martineau 根据case 切换MyClass 方法的非优化方式是什么?
  • 一种方法是让MyClass 成为自定义元类的实例。你也可以定义一个class factory

标签: python class inheritance mixins


【解决方案1】:

正如我在评论中所说,这种事情可以使用元类来完成。以下是如何使用一个:

class MixinA:
    def some_method(self):
        print('In MixinA.some_method()')


class MixinB:
    def some_method(self):
        print('In MixinB.some_method()')


class MyMetaClass(type):
    mixins = {'A': MixinA, 'B': MixinB}  # Supported mix-ins.

    def __new__(cls, name, bases, classdict, **kwargs):
        case = kwargs.pop('case')
        mixin = cls.mixins.get(case)
        if not mixin:
            raise TypeError(f'Unknown mix-in case selector {case!r} specified')
        else:
            bases += (mixin,)

        return type.__new__(cls, name, bases, classdict, **kwargs)


class ClassA(metaclass=MyMetaClass, case='A'): ...
class ClassB(metaclass=MyMetaClass, case='B'): ...

a = ClassA()
b = ClassB()

a.some_method()  # -> In MixinA.some_method()
b.some_method()  # -> In MixinB.some_method()


它也可以在没有元类的情况下完成,至少有两种方法。一种方法是利用 Python 3.6 中添加的 __init_subclass__() 特殊方法。

class MixinA:
    def some_method(self):
        print('In MixinA.some_method()')


class MixinB:
    def some_method(self):
        print('In MixinB.some_method()')


class MyClass:

    mixins = {'A': MixinA, 'B': MixinB}  # Supported mix-ins.

    def __init_subclass__(cls, /, case, **kwargs):
        super().__init_subclass__(**kwargs)
        mixin = cls.mixins.get(case)
        if not mixin:
            raise TypeError(f'Unknown mix-in case selector {case!r} specified')
        else:
            cls.__bases__ += (mixin,)


class ClassA(MyClass, case='A'): ...
class ClassB(MyClass, case='B'): ...


a = ClassA()
b = ClassB()

a.some_method()  # -> In MixinA.some_method()
b.some_method()  # -> In MixinB.some_method()


另一种方法是通过普通函数,这是可能的,因为在 Python 中,类本身就是“first class objects”。

这是一个通过我命名为 mixer() 的函数执行此操作的示例:

class MixinA:
    def some_method(self):
        print('In MixinA.some_method()')


class MixinB:
    def some_method(self):
        print('In MixinB.some_method()')


class MyClass: ...


MIXINS = {'A': MixinA, 'B': MixinB}  # Supported mix-in classes.

def mixer(cls, *, case):
    mixin = MIXINS.get(case)
    if not mixin:
        raise TypeError(f'Unknown mix-in case selector {case!r} specified')

    mixed_classname = f'{cls.__name__}Plus{mixin.__name__}'
    return type(mixed_classname, (cls, mixin), {})


ClassA = mixer(MyClass, case='A')
ClassB = mixer(MyClass, case='B')

a = ClassA()
b = ClassB()

a.some_method()  # -> In MixinA.some_method()
b.some_method()  # -> In MixinB.some_method()

如您所见,所有这些技术都受到class factory pattern(或多或少我们在这里处理的内容)中常见的限制,即基类或元类方法或指定函数必须知道子类或混合类的所有可能性才能使它们可用。

虽然有一些解决方法可以缓解这种情况 — 至少在使用其他方法时,请参阅我对 Improper use of __new__ to generate classes? 的回答,它可以与“类工厂”模式一起使用。

但是,在这种特定情况下,您可以通过简单地将混合类 class 作为参数而不是 case 选择器来避免混合类的硬编码。

【讨论】:

  • 我玩了一下这个,它似乎完全符合我的要求。三个简单的问题:(i)/ 参数对__init_subclass__ 的目的是什么? (ii) 为什么ClassAClassB 的定义中有省略号而不是pass?如果除了 2 个混合版本之外,所有方法都是共享的,那么没有理由在 ClassAClassB 中添加任何其他内容,对吧? (iii) 就我而言,这并不重要,因为混合方法不会重载 MyClass 中的任何内容,但通常交换 cls.__bases__ + (mixin,) 的顺序是否有意义?
  • (i) / 仅表示从那时起的关键字(您必须将 case 作为关键字传递)。 (ii) 省略号只是一个有效的占位符,就像pass 一样(但更好地传达了 IMO 的意图,并且短了一个字符,以便启动)。无论如何,在这种情况下没有理由添加任何其他内容。 (iii) 在基类元组中放置混入的位置将取决于您正在做什么——但通常混入不会重载与它们混合的类的方法。临别之想:既然我的回答如你所愿,请考虑采纳。
  • 我刚刚看到您的编辑 - 感谢您添加元类方法。当你提到它时,我想知道如何实现它。很高兴看到这两种方法并排。将混合直接作为参数传递也是个好主意。这使它更加干净。
  • 我添加了另一种可能是最简单的方法。同样,我建议您运行一些时序测试,看看您想要这样做的原因(更好的性能)是否值得麻烦。更复杂的代码是它自己的“开销”形式,你知道……
猜你喜欢
  • 1970-01-01
  • 2013-04-19
  • 2023-01-12
  • 2011-11-27
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-08-16
  • 2013-07-20
相关资源
最近更新 更多