在Python中,可以在不使用继承的情况下实现mixin行为吗?

发布于 2024-10-01 06:59:26 字数 510 浏览 4 评论 0原文

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 行为。

Is there a reasonable way in Python to implement mixin behavior similar to that found in Ruby -- that is, without using inheritance?

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)

I had a vague idea to do this with a class decorator, but my attempts led to confusion. Most of my searches on the topic have led in the direction of using inheritance (or in more complex scenarios, multiple inheritance) to achieve mixin behavior.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(8

少女七分熟 2024-10-08 06:59:26
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
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)

output:

a()
b()
c()
d()
e()
False
年少掌心 2024-10-08 06:59:26

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

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

You can add the methods as functions:

Foo.b = Mixin.b.im_func
Foo.c = Mixin.c.im_func
千笙结 2024-10-08 06:59:26

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

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

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

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

换句话说:一个与mixin 被注入到继承层次结构中。所以,Ruby 实际上确实使用继承来进行 mixin 组合。

我在上面遗漏了一些细节:首先,该模块实际上并没有作为 C 的超类插入,而是作为 C 的超类插入'(这是C 的单例类)超类。其次,如果 mixin 本身已混合在其他 mixin 中,那么这些也会被包装到直接插入到 α 上方的伪类中,并且此过程会递归地应用,在如果混合的 mixin 又具有 mixin。

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

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

I am not that familiar with Python, but from what I know about Python metaprogramming, you could actually do it pretty much the same way it is done in Ruby.

In Ruby, a module basically consists of two things: a pointer to a method dictionary and a pointer to a constant dictionary. A class consists of three things: a pointer to a method dictionary, a pointer to a constant dictionary and a pointer to the superclass.

When you mix in a module M into a class C, the following happens:

  1. an anonymous class α is created (this is called an include class)
  2. α's method dictionary and constant dictionary pointers are set equal to M's
  3. α's superclass pointer is set equal to C's
  4. C's superclass pointer is set to α

In other words: a fake class which shares its behavior with the mixin is injected into the inheritance hierarchy. So, Ruby actually does use inheritance for mixin composition.

I left out a couple of subleties above: first off, the module doesn't actually get inserted as C's superclass, it gets inserted as C's superclasses' (which is C's singleton class) superclass. And secondly, if the mixin itself has mixed in other mixins, then those also get wrapped into fake classes which get inserted directly above α, and this process is applied recursively, in case the mixed in mixins in turn have mixins.

Basically, the whole mixin hierarchy gets flattened into a straight line and spliced into the inheritance chain.

AFAIK, Python actually allows you to change a class's superclass(es) after the fact (something which Ruby does not allow you to do), and it also gives you access to a class's dict (again, something that is impossible in Ruby), so you should be able to implement this yourself.

黄昏下泛黄的笔记 2024-10-08 06:59:26

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

您可以使用元类工厂:

 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

EDIT: Fixed what could (and probably should) be construed as a bug. Now it builds a new dict and then updates that from the class's dict. This prevents mixins from overwriting methods that are defined directly on the class. The code is still untested but should work. I'm busy ATM so I'll test it later. It worked fine except for a syntax error. In retrospect, I decided that I don't like it (even after my further improvements) and much prefer my other solution even if it is more complicated. The test code for that one applies here as well but I wont duplicate it.

You could use a metaclass factory:

 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 

then use it like:

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

     # rest of the stuff
偏爱自由 2024-10-08 06:59:26

这是基于 ruby​​ 中的完成方式 由 Jörg W Mittag 解释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)

This one is based on the way it's done in ruby as explained by Jörg W Mittag. All of the wall of code after if __name__=='__main__' is test/demo code. There's actually only 13 lines of real code to it.

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)
留蓝 2024-10-08 06:59:26

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

You could decorate the classes __getattr__ to check in the mixin. The problem is that all methods of the mixin would always require an object the type of the mixin as their first parameter, so you would have to decorate __init__ as well to create a mixin-object. I believe you could achieve this using a class decorator.

薆情海 2024-10-08 06:59:26
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
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)

This basically uses the Mixin class as a container to hold ad-hoc functions (not methods) that behave like methods by taking an object instance (self) as the first argument. __getattr__ will redirect missing calls to these methods-alike functions.

This passes your simple tests as shown below. But I cannot guarantee it will do all the things you want. Make more thorough test to make sure.

$ python mixin.py 
a()
b()
c()
False
‖放下 2024-10-08 06:59:26

作品?这似乎是处理此问题的最简单方法:要么将对象包装在装饰器中,要么将方法作为对象导入到类定义本身中。这就是我通常所做的:将我想要在类之间共享的方法放在一个文件中,然后导入该文件。如果我想覆盖某些行为,我会导入一个修改后的文件,其方法名称与对象名称相同。虽然有点草率,但确实有效。

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

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

就像我说的,它很草率。

Composition? It seems like that would be the simplest way to handle this: either wrap your object in a decorator or just import the methods as an object into your class definition itself. This is what I usually do: put the methods that I want to share between classes in a file and then import the file. If I want to override some behavior I import a modified file with the same method names as the same object name. It's a little sloppy, but it works.

For example, if I want the init_covers behavior from this file (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))    

In my bar class file I would do: import bedg as foo

and then if I want to change my foo behaviors in another class that inherited bar, I write

import bild as foo

Like I say, it is sloppy.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文