Обсуждение множественного наследования vs Composition для проекта (+ прочее)

Я пишу платформу на Python для моделирования распределенных скоплений датчиков. Идея состоит в том, что конечный пользователь может написать собственный узел, состоящий из поведения SensorNode (связь, ведение журнала и т. Д.), А также реализации ряда различных датчиков.

Пример ниже кратко демонстрирует концепцию.

#prewritten
class Sensor(object):
  def __init__(self):
    print "Hello from Sensor"
  #...

#prewritten
class PositionSensor(Sensor):
  def __init__(self):
    print "Hello from Position"
    Sensor.__init__(self)
  #...

#prewritten
class BearingSensor(Sensor):
  def __init__(self):
    print "Hello from Bearing"
    Sensor.__init__(self)
  #...

#prewritten
class SensorNode(object):
  def __init__(self):
    print "Hello from SensorNode"
  #...

#USER WRITTEN
class MySensorNode(SensorNode,BearingSensor,PositionSensor):
  def CustomMethod(self):
    LogData={'Position':position(), 'Bearing':bearing()} #position() from PositionSensor, bearing() from BearingSensor
    Log(LogData) #Log() from SensorNode

НОВОЕ РЕДАКТИРОВАНИЕ:

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

Основная цель этого проекта - разработать платформу моделирования, которая предоставляет абстрактные интерфейсы для датчиков, так что те же реализованные пользователем функции могут быть напрямую перенесены на роботизированный рой, работающий со встроенным Linux. Поскольку целью является внедрение роботов, мне нужно спроектировать так, чтобы программный узел вел себя так же и имел доступ только к той информации, которую мог бы иметь физический узел.

В рамках механизма моделирования я предоставлю набор классов, моделирующих различные типы датчиков и различные типы сенсорных узлов. Я хочу отвлечь всю эту сложность от пользователя, чтобы все, что пользователь должен был сделать, это определить, какие датчики присутствуют на узле, и какой тип узла датчиков (мобильный, фиксированное положение) реализуется.

Я изначально думал, что каждый датчик будет предоставлять метод read (), который будет возвращать соответствующие значения, однако, прочитав ответы на вопрос, я вижу, что, возможно, более описательные имена методов будут полезны (.distance (), .position ( ), .bearing () и т. д.).

Изначально я хотел использовать отдельные классы для датчиков (с общими предками), чтобы более опытный пользователь мог легко расширить один из существующих классов для создания нового датчика, если захочет. Например:

Sensor
  |
DistanceSensor(designed for 360 degree scan range)
    |           |           |
IR Sensor   Ultrasonic    SickLaser
(narrow)    (wider)       (very wide)

Причина, по которой я изначально думал о множественном наследовании (хотя он частично нарушает отношения наследования IS-A), была связана с принципом, лежащим в основе системы моделирования. Позволь мне объяснить:

Реализуемый пользователем MySensorNode не должен иметь прямого доступа к своему положению в среде (аналогично роботу, доступ осуществляется косвенно через интерфейс датчика), аналогично, датчики не должны знать, где они находятся. Однако этот недостаток прямых знаний создает проблему, поскольку все возвращаемые значения датчиков зависят от их положения и ориентации в окружающей среде (что необходимо смоделировать для получения правильных значений).

SensorNode, как класс, реализованный в библиотеках моделирования, отвечает за отрисовку MySensorNode в среде pygame - таким образом, это единственный класс, который должен иметь прямой доступ к положению и ориентации узла датчика в среде.

SensorNode также отвечает за перемещение и вращение в окружающей среде, однако это перемещение и вращение является побочным эффектом приведения в действие двигателя.

Я имею в виду, что роботы не могут напрямую изменять свое положение в мире, все, что они могут делать, - это обеспечивать двигатели энергией, а движение внутри мира является побочным эффектом взаимодействия двигателей с окружающей средой. Мне нужно точно смоделировать это в симуляции.

Итак, для перемещения реализованные пользователем функции могут использовать:

motors(50,50)

Этот вызов, как побочный эффект, изменит положение узла в мире.

Если SensorNode был реализован с использованием композиции, SensorNode.motors (...) не сможет напрямую изменять переменные экземпляра (например, положение), а MySensorNode.draw () не будет преобразован в SensorNode.draw (), поэтому SensorNode imo должен быть реализовано с использованием наследования.

Что касается датчиков, то преимущество композиции для такой проблемы очевидно, MySensorNode состоит из нескольких датчиков - достаточно сказано.

Однако проблема, как я вижу, в том, что датчикам нужен доступ к их положению и ориентации в мире, и если вы используете композицию, вы получите такой вызов, как:

>>> PosSensor.position((123,456))
(123,456)

Опять же - подумав, вы можете передать себя датчику при инициализации, например:

PosSensor = PositionSensor(self)

тогда позже

PosSensor.position()

однако этому PosSensor.position () затем потребуется доступ к информации, локальной для экземпляра (переданной как self во время init ()), так зачем вообще вызывать PosSensor, если вы можете получить доступ к информации локально? Также передача вашего экземпляра объекту, из которого вы состоите, просто кажется не совсем правильным, пересекая границы инкапсуляции и сокрытия информации (хотя python мало что делает для поддержки идеи сокрытия информации).

Если бы решение было реализовано с использованием множественного наследования, эти проблемы исчезли бы:

class MySensorNode(SensorNode,PositionSensor,BearingSensor):
  def Think():
    while bearing()>0:
      # bearing() is provided by BearingSensor and in the simulator
      # will simply access local variables provided by SensorNode
      # to return the bearing. In robotic implementation, the
      # bearing() method will instead access C routines to read
      # the actual bearing from a compass sensor
      motors(100,-100)
      # spin on the spot, will as a side-effect alter the return
      # value of bearing()

    (Ox,Oy)=position() #provided by PositionSensor
    while True:
      (Cx,Cy)=position()
      if Cx>=Ox+100:
        break
      else:
        motors(100,100)
        #full speed ahead!will alter the return value of position()

Надеюсь, это изменение прояснило некоторые вещи. Если у вас есть какие-либо вопросы, я более чем счастлив попытаться их прояснить.

СТАРЫЕ ВЕЩИ:

Когда создается объект типа MySensorNode, необходимо вызвать все конструкторы из суперклассов. Я не хочу усложнять пользователя написанием специального конструктора для MySensorNode, который вызывает конструктор из каждого суперкласса. В идеале я бы хотел:

mSN = MySensorNode()
# at this point, the __init__() method is searched for
# and SensorNode.__init__() is called given the order
# of inheritance in MySensorNode.__mro__

# Somehow, I would also like to call all the other constructors
# that were not executed (ie BearingSensor and PositionSensor)

Любое понимание или общие комментарии будут оценены, Ура :)

СТАРЫЙ РЕДАКТИРОВАНИЕ: делать что-то вроде:

#prewritten
class SensorNode(object):
  def __init__(self):
    print "Hello from SensorNode"
    for clss in type(self).__mro__:
      if clss!=SensorNode and clss!=type(self):
        clss.__init__(self)

Это работает, поскольку self является экземпляром MySensorNode. Однако это решение беспорядочное.



person Mike Hamer    schedule 14.03.2009    source источник
comment
Есть ли способ сократить это и сфокусировать это? На данный момент это почти непонятно.   -  person S.Lott    schedule 14.03.2009


Ответы (3)


Архитектура датчика может быть решена с помощью композиции, если вы хотите придерживаться исходного дизайна карты данных. Кажется, вы новичок в Python, поэтому я постараюсь свести идиомы к минимуму.

class IRSensor:
    def read(self): return {'ir_amplitude': 12}

class UltrasonicSensor:
    def read(self): return {'ultrasonic_amplitude': 63}

class SickLaserSensor:
    def read(self): return {'laser_amplitude': 55}

class CompositeSensor:
    """Wrap multiple component sensors, coalesce the results, and return
    the composite readout.
    """
    component_sensors = []

    def __init__(self, component_sensors=None):
        component_sensors = component_sensors or self.component_sensors
        self.sensors = [cls() for cls in component_sensors]

    def read(self):
        measurements = {}
        for sensor in self.sensors:
            measurements.update(sensor.read())
        return measurements

class MyCompositeSensor(CompositeSensor):
    component_sensors = [UltrasonicSensor, IRSensor]


composite_sensor = MyCompositeSensor()
measurement_map = composite_sensor.read()
assert measurement_map['ultrasonic_amplitude'] == 63
assert measurement_map['ir_amplitude'] == 12

Архитектурная проблема, которую вы описываете с исполнительными механизмами, решается с помощью миксинов и проксирования (через __getattr__), а не наследования. (Проксирование может быть хорошей альтернативой наследованию, поскольку объекты для прокси-сервера могут быть привязаны / отвязаны во время выполнения. Кроме того, вам не нужно беспокоиться об обработке всей инициализации в одном конструкторе с использованием этой техники.)

class MovementActuator:
    def __init__(self, x=0, y=0):
        self.x, self.y = (x, y)

    def move(self, x, y):
        print 'Moving to', x, y
        self.x, self.y = (x, y)

    def get_position(self):
        return (self.x, self.y)

class CommunicationActuator:
    def communicate(self):
        return 'Hey you out there!'

class CompositeActuator:
    component_actuators = []

    def __init__(self, component_actuators=None):
        component_actuators = component_actuators \
            or self.component_actuators
        self.actuators = [cls() for cls in component_actuators]

    def __getattr__(self, attr_name):
        """Look for value in component sensors."""
        for actuator in self.actuators:
            if hasattr(actuator, attr_name):
                return getattr(actuator, attr_name)
        raise AttributeError(attr_name)


class MyCompositeActuator(CompositeActuator):
    component_actuators = [MovementActuator, CommunicationActuator]

composite_actuator = MyCompositeActuator()
assert composite_actuator.get_position() == (0, 0)
assert composite_actuator.communicate() == 'Hey you out there!'

И, наконец, вы можете скинуть все это вместе с простым объявлением узла:

from sensors import *
from actuators import *

class AbstractNode:
    sensors = [] # Set of classes.
    actuators = [] # Set of classes.
    def __init__(self):
        self.composite_sensor = CompositeSensor(self.sensors)
        self.composite_actuator = CompositeActuator(self.actuators)

class MyNode(AbstractNode):
    sensors = [UltrasonicSensor, SickLaserSensor]
    actuators = [MovementActuator, CommunicationActuator]

    def think(self):
        measurement_map = self.composite_sensor.read()
        while self.composite_actuator.get_position()[1] >= 0:
            self.composite_actuator.move(100, -100)

my_node = MyNode()
my_node.think()

Это должно дать вам представление об альтернативах жесткой системе типов. Обратите внимание, что вам вообще не нужно полагаться на иерархию типов - просто реализуйте (потенциально неявный) общий интерфейс.

МЕНЬШЕ СТАРОЕ:

Прочитав вопрос более внимательно, я понял, что у вас есть классический пример наследования алмаза , которое является злом, которое заставляет людей бегать в сторону единственного наследования.

Вероятно, вы не хотите, чтобы это начиналось, поскольку иерархия классов в Python означает приседание. Что вы хотите сделать, так это создать SensorInterface (минимальные требования к датчику) и иметь кучу классов «миксинов» с полностью независимыми функциями, которые можно вызывать с помощью методов с разными именами. В своей сенсорной структуре вы не должны говорить что-то вроде isinstance(sensor, PositionSensor) - вы должны говорить что-то вроде "может ли этот датчик определять местоположение?" в следующем виде:

def get_position(sensor):
    try:
        return sensor.geolocate()
    except AttributeError:
        return None

Это суть философии утиного набора текста и EAFP (проще просить прощения, чем Permission), оба из которых поддерживает язык Python.

Вам, вероятно, следует описать, какие методы эти датчики фактически будут реализовывать, чтобы мы могли описать, как вы можете использовать классы миксинов для своей архитектуры плагина.

СТАРЫЙ:

Если они напишут код в модуле, который помещается в пакет плагина или что-то еще, вы можете волшебным образом инструментировать классы для них, когда вы импортируете их модули плагинов. Что-то вроде этого фрагмента (непроверено):

 import inspect
 import types

 from sensors import Sensor

 def is_class(obj):
     return type(obj) in (types.ClassType, types.TypeType)

 def instrumented_init(self, *args, **kwargs):
     Sensor.__init__(self, *args, **kwargs)

 for module in plugin_modules: # Get this from somewhere...
     classes = inspect.getmembers(module, predicate=is_class)
     for name, cls in classes:
         if hasattr(cls, '__init__'):
             # User specified own init, may be deriving from something else.
             continue 
         if cls.__bases__ != tuple([Sensor]):
             continue # Class doesn't singly inherit from sensor.
         cls.__init__ = instrumented_init

Вы можете найти модули в пакете с другой функцией.

person cdleary    schedule 14.03.2009
comment
+1: композиция обычно работает лучше, чем сложные иерархии наследования. - person S.Lott; 14.03.2009

super вызывает следующий класс в mro-списке. Это работает, даже если вы не укажете __init__ форму какого-либо класса.

class A(object):
  def __init__(self):
    super(A,self).__init__()
    print "Hello from A!"

class B(A):
  def __init__(self):
    super(B,self).__init__()
    print "Hello from B!"

class C(A):
  def __init__(self):
    super(C,self).__init__()
    print "Hello from C!"

class D(B,C):
  def __init__(self):
    super(D,self).__init__()
    print "Hello from D!"

class E(B,C):
  pass

Пример:

>>> x = D()
Hello from A!
Hello from C!
Hello from B!
Hello from D!
>>> y = E()
Hello from A!
Hello from C!
Hello from B!
>>> 

Изменить: переписал ответ. (снова)

person Markus Jarderot    schedule 14.03.2009
comment
(Примечание: это все равно вызовет базовый класс на пике наследования класса три раза, с потенциально катастрофическими результатами.) - person cdleary; 14.03.2009

Вот частичное решение:

class NodeMeta(type):
    def __init__(cls, name, bases, d):
        setattr(cls, '__inherits__', bases)

class Node(object):
    __metaclass__ = NodeMeta

    def __init__(self):
        for cls in self.__inherits__:
            cls.cls_init(self)

class Sensor(Node):
    def cls_init(self):
        print "Sensor initialized"

class PositionSensor(Sensor):
    def cls_init(self):
        print "PositionSensor initialized"
        self._bearing = 0

    def bearing(self):
        # calculate bearing:
        return self._bearing

class BearingSensor(Sensor):
    def cls_init(self):
        print "BearingSensor initialized"
        self._position = (0, 0)

    def position(self):
        # calculate position:
        return self._position

# -------- custom sensors --------

class CustomSensor(PositionSensor, BearingSensor):
    def think(self):
        print "Current position:", self.position()
        print "Current bearing:", self.bearing()

class CustomSensor2(PositionSensor, BearingSensor, Sensor):
    pass

>>> s = CustomSensor()
PositionSensor initialized
BearingSensor initialized
>>> s.think()
Current position: (0, 9)
Current bearing: 0

Вам придется переместить свой __init__ код из подклассов Node в какой-нибудь другой метод (я использовал cls_init).

Изменить: я опубликовал это до того, как увидел ваши обновления; Я перечитаю ваш вопрос и при необходимости обновлю это решение.

person elo80ka    schedule 14.03.2009