В Python можно реализовать смешанное поведение без использования наследования? - PullRequest
4 голосов
/ 10 ноября 2010

Есть ли в Python разумный способ реализовать поведение mixin, подобное тому, которое есть в 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)

У меня была смутная идея сделать это с декоратором классовМои попытки привели к путанице.Большинство моих поисков по этой теме привели к использованию наследования (или в более сложных сценариях множественного наследования) для достижения смешанного поведения.

Ответы [ 8 ]

9 голосов
/ 10 ноября 2010
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
4 голосов
/ 10 ноября 2010

Вы можете добавить методы как функции:

Foo.b = Mixin.b.im_func
Foo.c = Mixin.c.im_func
3 голосов
/ 10 ноября 2010

Это основано на том, как это сделано в рубине, как , объясненное Йоргом Миттагом Вся стена кода после 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)
3 голосов
/ 10 ноября 2010

Я не настолько знаком с Python, но из того, что я знаю о метапрограммировании Python, вы могли бы сделать это почти так же, как это делается в Ruby.

В Ruby модуль в основном состоит из двух вещей: указатель на словарь методов и указатель на словарь констант. Класс состоит из трех вещей: указатель на словарь методов, указатель на словарь констант и указатель на суперкласс.

Когда вы смешиваете модуль M в класс C, происходит следующее:

  1. создается анонимный класс α (это называется include class )
  2. α и указатели словаря констант установлены равными M '*
  3. α установлен равным C '*
  4. C установлен на α

Другими словами: фальшивый класс, который делится своим поведением с миксином, внедряется в иерархию наследования. Таким образом, Ruby на самом деле использует наследование для состава mixin.

Я пропустил пару пунктов выше: во-первых, модуль фактически не вставляется как суперкласс C, он вставляется как суперклассы C (то есть C). синглтон класс) суперкласс. И, во-вторых, если сам миксин смешался с другими миксинами, то эти также будут обернуты в поддельные классы, которые вставляются непосредственно над α, и этот процесс применяется рекурсивно, в случае, если миксин смешан в включаю миксин.

По сути, вся иерархия миксинов сглаживается в прямую линию и объединяется в цепочку наследования.

AFAIK, Python фактически позволяет вам изменить суперкласс (ы) класса после факта (то, что Ruby делает , а не позволяет вам это сделать), и он также дает вам доступ к dict класса (опять же, что-то, что невозможно в Ruby), так что вы сможете реализовать это самостоятельно.

3 голосов
/ 10 ноября 2010

EDIT: исправлено то, что могло (и, вероятно, должно) быть истолковано как ошибка.Теперь он создает новый диктат, а затем обновляет его из классового диктата.Это предотвращает перезапись миксинами методов, которые определены непосредственно в классе. Код еще не проверен, но должен работать.Я занят банкоматом, поэтому протестирую его позже. Он работал нормально, за исключением синтаксической ошибки.Оглядываясь назад, я решил, что мне это не нравится (даже после моих дальнейших улучшений), и я предпочитаю мое другое решение , даже если оно более сложное.Тестовый код для этого также применим и здесь, но я не буду его дублировать.

Вы можете использовать фабрику метаклассов:

 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
0 голосов
/ 10 ноября 2010

Состав?Кажется, что это будет самый простой способ справиться с этим: либо обернуть ваш объект в декоратор, либо просто импортировать методы как объект в само определение вашего класса.Это то, что я обычно делаю: поместите методы, которыми я хочу поделиться между классами, в файл, а затем импортируйте файл.Если я хочу изменить какое-либо поведение, я импортирую модифицированный файл с теми же именами методов, что и имя объекта.Это немного небрежно, но работает.

Например, если я хочу поведение 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))    

В моем файле класса бара я быdo: import bedg as foo

и затем, если я хочу изменить поведение foo в другом классе, унаследованном bar, я пишу

import bild as foo

Как я уже сказалнебрежно.

0 голосов
/ 10 ноября 2010
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 в качестве контейнера для хранения ad-hoc функций (не методов), которые ведут себя как методы, принимая экземпляр объекта (self) в качестве первого аргумента.__getattr__ перенаправит пропущенные вызовы на эти методы-методы.

Это пройдет ваши простые тесты, как показано ниже.Но я не могу гарантировать, что он будет делать все, что вы хотите.Сделайте более тщательный тест, чтобы убедиться.

$ python mixin.py 
a()
b()
c()
False
0 голосов
/ 10 ноября 2010

Вы можете украсить классы __getattr__, чтобы проверить в миксине.Проблема заключается в том, что для всех методов mixin всегда требуется объект типа mixin в качестве их первого параметра, поэтому для создания объекта mixin вам также необходимо украсить __init__.Я полагаю, что вы могли бы достичь этого, используя декоратор класса .

...