在Python中可以实现mixin行为而不使用继承吗?

时间:2010-11-09 22:47:37

标签: python ruby inheritance mixins

在Python中是否有合理的方法来实现类似于Ruby中的mixin行为 - 也就是说,不使用继承?

class Mixin(object):
    def b(self): print "b()"
    def c(self): print "c()"

class Foo(object):
    # Somehow mix in the behavior of the Mixin class,
    # so that all of the methods below will run and
    # the issubclass() test will be False.

    def a(self): print "a()"

f = Foo()
f.a()
f.b()
f.c()
print issubclass(Foo, Mixin)

我有一个模糊的想法,与类装饰师这样做,但我的尝试导致混乱。我对该主题的大多数搜索都导致了使用继承(或者在更复杂的场景中,多重继承)来实现mixin行为。

8 个答案:

答案 0 :(得分:9)

def mixer(*args):
    """Decorator for mixing mixins"""
    def inner(cls):
        for a,k in ((a,k) for a in args for k,v in vars(a).items() if callable(v)):
            setattr(cls, k, getattr(a, k).im_func)
        return cls
    return inner

class Mixin(object):
    def b(self): print "b()"
    def c(self): print "c()"

class Mixin2(object):
    def d(self): print "d()"
    def e(self): print "e()"


@mixer(Mixin, Mixin2)
class Foo(object):
    # Somehow mix in the behavior of the Mixin class,
    # so that all of the methods below will run and
    # the issubclass() test will be False.

    def a(self): print "a()"

f = Foo()
f.a()
f.b()
f.c()
f.d()
f.e()
print issubclass(Foo, Mixin)

输出:

a()
b()
c()
d()
e()
False

答案 1 :(得分:4)

您可以将这些方法添加为函数:

Foo.b = Mixin.b.im_func
Foo.c = Mixin.c.im_func

答案 2 :(得分:3)

编辑:修正了可能(并且可能应该)被解释为错误的内容。现在它构建一个新的dict,然后从类的dict中更新它。这可以防止mixins覆盖直接在类上定义的方法。 代码仍然未经测试但应该有效。我忙于ATM,所以我稍后会测试它。除了语法错误外,它工作正常。回想起来,我认为我不喜欢它(即使在我进一步改进之后)并且更喜欢my other solution,即使它更复杂。这个测试代码也适用于此,但我不会复制它。

您可以使用元类工厂:

 import inspect

 def add_mixins(*mixins):
     Dummy = type('Dummy', mixins, {})
     d = {}

     for mixin in reversed(inspect.getmro(Dummy)):
         d.update(mixin.__dict__)

     class WithMixins(type):
         def __new__(meta, classname, bases, classdict):
             d.update(classdict)
             return super(WithMixins, meta).__new__(meta, classname, bases, d)
     return WithMixins 

然后使用它:

 class Foo(object):
     __metaclass__ = add_mixins(Mixin1, Mixin2)

     # rest of the stuff

答案 3 :(得分:3)

我对Python并不熟悉,但根据我对Python元编程的了解,你实际上可以像在Ruby中那样完成它。

在Ruby中,模块基本上由两部分组成:指向方法字典的指针和指向常量字典的指针。一个类由三部分组成:指向方法字典的指针,指向常量字典的指针和指向超类的指针。

当您将模块M混合到类C中时,会发生以下情况:

  1. 创建了一个匿名类α(这称为包含类
  2. α的方法字典和常量字典指针设置为M
  3. α的超类指针设置为C
  4. C的超类指针设置为α
  5. 换句话说:与mixin共享其行为的假类被注入到继承层次结构中。所以,Ruby实际上 使用继承来进行mixin组合。

    我遗漏了以上几个问题:首先,模块实际上并没有作为C的超类插入,而是作为C的超类插入(这是{ {1}}的单例类)超类。其次,如果mixin本身在其他mixins中混合了,那么那些也会被包装到伪类中,这些类直接插入到C之上,并且这个过程是递归应用的,以防混合在mixins中又有mixins。

    基本上,整个mixin层次结构变平直,并拼接到继承链中。

    AFAIK,Python实际上允许你在事实之后改变一个类的超类(Ruby使允许你做的事情),并且它还允许你访问类的{{ 1}}(再次,Ruby中不可能的事情),所以你应该能够自己实现它。

答案 4 :(得分:3)

这个基于它在ruby中的行为explained by Jörg W Mittagif __name__=='__main__'之后的所有代码墙都是测试/演示代码。实际上只有13行真实代码。

import inspect

def add_mixins(*mixins):
    Dummy = type('Dummy', mixins, {})
    d = {}

    # Now get all the class attributes. Use reversed so that conflicts
    # are resolved with the proper priority. This rules out the possibility
    # of the mixins calling methods from their base classes that get overridden
    # using super but is necessary for the subclass check to fail. If that wasn't a
    # requirement, we would just use Dummy above (or use MI directly and
    # forget all the metaclass stuff).

    for base in reversed(inspect.getmro(Dummy)):
        d.update(base.__dict__)

    # Create the mixin class. This should be equivalent to creating the
    # anonymous class in Ruby.
    Mixin = type('Mixin', (object,), d)

    class WithMixins(type):
        def __new__(meta, classname, bases, classdict):
            # The check below prevents an inheritance cycle from forming which
            # leads to a TypeError when trying to inherit from the resulting
            # class.
            if not any(issubclass(base, Mixin) for base in bases):
                # This should be the the equivalent of setting the superclass 
                # pointers in Ruby.
                bases = (Mixin,) + bases
            return super(WithMixins, meta).__new__(meta, classname, bases,
                                                   classdict)

    return WithMixins 


if __name__ == '__main__':

    class Mixin1(object):
        def b(self): print "b()"
        def c(self): print "c()"

    class Mixin2(object):
        def d(self): print "d()"
        def e(self): print "e()"

    class Mixin3Base(object):
        def f(self): print "f()"

    class Mixin3(Mixin3Base): pass

    class Foo(object):
        __metaclass__ = add_mixins(Mixin1, Mixin2, Mixin3)

        def a(self): print "a()"

    class Bar(Foo):
        def f(self): print "Bar.f()"

    def test_class(cls):
        print "Testing {0}".format(cls.__name__)
        f = cls()
        f.a()
        f.b()
        f.c()
        f.d()
        f.e()
        f.f()
        print (issubclass(cls, Mixin1) or 
               issubclass(cls, Mixin2) or
               issubclass(cls, Mixin3))

    test_class(Foo)
    test_class(Bar)

答案 5 :(得分:0)

你可以装饰班级__getattr__来检查混音。问题是mixin的所有方法总是需要一个对象作为第一个参数mixin的类型,所以你必须装饰__init__以创建一个mixin-object。我相信你可以使用class decorator来实现这一点。

答案 6 :(得分:0)

from functools import partial
class Mixin(object):
    @staticmethod
    def b(self): print "b()"
    @staticmethod
    def c(self): print "c()"

class Foo(object):
    def __init__(self, mixin_cls):
        self.delegate_cls = mixin_cls

    def __getattr__(self, attr):
        if hasattr(self.delegate_cls, attr):
            return partial(getattr(self.delegate_cls, attr), self)

    def a(self): print "a()"

f = Foo(Mixin)
f.a()
f.b()
f.c()
print issubclass(Foo, Mixin)

这基本上使用Mixin类作为容器,通过将对象实例(self)作为第一个参数来保存行为类似于方法的 ad-hoc 函数(而不是方法)。 __getattr__会将缺少的调用重定向到这些类似方法的函数。

这将通过您的简单测试,如下所示。但我不能保证它会做你想要的所有事情。进行更彻底的测试以确保。

$ python mixin.py 
a()
b()
c()
False

答案 7 :(得分:0)

组合物?看起来这是处理这个问题的最简单方法:将对象包装在装饰器中,或者只是将方法作为对象导入到类定义本身中。这就是我通常做的事情:将我想要在类之间共享的方法放在文件中,然后导入文件。如果我想覆盖某些行为,我导入一个修改过的文件,其方法名称与同一对象名称相同。这有点草率,但它确实有效。

例如,如果我想要此文件中的init_covers行为(bedg.py)

import cove as cov


def init_covers(n):
    n.covers.append(cov.Cover((set([n.id]))))
    id_list = []
    for a in n.neighbors:
        id_list.append(a.id)
    n.covers.append(cov.Cover((set(id_list))))

def update_degree(n):
    for a in n.covers:
        a.degree = 0
        for b in n.covers:
            if  a != b:
                a.degree += len(a.node_list.intersection(b.node_list))    

在我的bar类文件中,我会这样做:import bedg as foo

然后如果我想在继承bar的另一个类中更改我的foo行为,我会写

import bild as foo

就像我说的那样,它很草率。