5

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

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)

我有一个模糊的想法,用类装饰器来做这件事,但我的尝试导致了混乱。我对该主题的大部分搜索都指向使用继承(或者在更复杂的场景中,多重继承)来实现混合行为的方向。

4

8 回答 8

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
于 2010-11-10T10:35:09.350 回答
4

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

Foo.b = Mixin.b.im_func
Foo.c = Mixin.c.im_func
于 2010-11-09T22:59:37.193 回答
3

正如Jörg W Mittag所解释的那样,这是基于它在 ruby​​ 中完成的方式。之后的所有代码墙if __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)
于 2010-11-10T12:04:53.510 回答
3

编辑:修复了可能(并且可能应该)被解释为错误的问题。现在它构建了一个新的字典,然后从类的字典中更新它。这可以防止 mixin 覆盖直接在类上定义的方法。该代码仍然未经测试,但应该可以工作。我正忙着提款机,所以我稍后会测试它。除了语法错误外,它工作正常。回想起来,我决定我不喜欢它(即使在我进一步改进之后)并且更喜欢我的其他解决方案,即使它更复杂。该测试代码也适用于此处,但我不会复制它。

您可以使用元类工厂:

 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
于 2010-11-09T22:57:59.100 回答
3

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

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

当您将一个模块M混入一个类C时,会发生以下情况:

  1. 创建了一个匿名类α(这称为包含类
  2. αM的方法字典和常量字典指针设置为等于
  3. αC的超类指针设置为等于
  4. C的超类指针设置为α

换句话说:一个与 mixin 共享其行为的假类被注入到继承层次结构中。因此,Ruby 实际上确实使用继承来进行 mixin 组合。

我在上面遗漏了几个子组件:首先,模块实际上并没有作为“超类”C插入,而是作为C“超类”(即C“单例类”)超类插入。其次,如果 mixin 本身已经混入了其他 mixins,那么这些也会被包装到直接插入到上面的假类中α,并且这个过程是递归地应用的,以防混入的 mixins 又具有 mixins。

基本上,整个 mixin 层次结构被扁平化为一条直线并拼接到继承链中。

AFAIK,Python 实际上允许您在事后更改类的超类(Ruby不允许您这样做),并且它还允许您访问类的dict(再次,这在 Ruby 中是不可能的),所以你应该能够自己实现这个。

于 2010-11-09T23:27:50.140 回答
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)作为第一个参数来表现得像方法。__getattr__会将丢失的调用重定向到这些类似方法的函数。

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

$ python mixin.py 
a()
b()
c()
False
于 2010-11-09T23:13:35.627 回答
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))    

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

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

import bild as foo

就像我说的,它很草率。

于 2010-11-10T08:49:15.040 回答
0

您可以装饰类__getattr__以签入 mixin。问题是 mixin 的所有方法总是需要一个 mixin 类型的对象作为它们的第一个参数,因此您还必须进行装饰__init__以创建一个 mixin 对象。我相信你可以使用类装饰器来实现这一点。

于 2010-11-09T22:58:35.153 回答