Обсуждение множественного наследования и композиции для проекта (+ другие вещи)
Я пишу платформу python для моделирования распределенных датчиков. Идея состоит в том, что конечный пользователь может написать пользовательский Node, состоящий из поведения 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. Поскольку роботизированная реализация является целью, мне нужно сконструировать так, чтобы программное обеспечение Node вело себя одинаково и имело только доступ к информации, которую имел бы физический Node.
В качестве части механизма моделирования я буду предлагать набор классов, имитирующих различные типы датчиков и различные типы датчиков node. Я хочу отвлечь всю эту сложность от пользователя, чтобы все, что должен сделать пользователь, определить, какие датчики присутствуют на node, и какой тип датчика Node (мобильное фиксированное положение) реализуется.
Мое первоначальное мышление заключалось в том, что каждый датчик предоставит метод 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 - таким образом, это единственный класс, который должен иметь прямой доступ к положению и ориентации датчика Node в пределах окружающая среда.
SensorNode также отвечает за перевод и вращение в среде, однако этот перевод и вращение являются побочным эффектом приведения в действие двигателя.
Что я имею в виду, так это то, что роботы не могут напрямую изменить свое положение в мире, все, что они могут сделать, это обеспечить мощность для двигателей, а движение в мире - это побочный эффект взаимодействия моторов с окружающей средой. Мне нужно точно это моделировать в симуляции.
Итак, чтобы двигаться, пользовательская функциональность может использовать:
motors(50,50)
Этот вызов в качестве побочного эффекта изменит положение Node в мире.
Если 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)
Любое понимание или общие комментарии будут оценены,
Приветствия:)
OLD EDIT:
Выполнение чего-то вроде:
#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)
Это работает, поскольку сам является экземпляром MySensorNode. Однако это решение беспорядочно.
Ответы
Ответ 1
Архитектура датчика может быть решена с использованием композиции, если вы хотите придерживаться первоначального дизайна карты данных. Вы, кажется, новичок в 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
Архитектурная проблема, которую вы описываете с помощью исполнительных механизмов, решается с помощью mixins и проксирования (через __getattr__
), а не наследования. (Proxying может быть хорошей альтернативой наследованию, поскольку объекты для прокси-сервера могут быть связаны/несвязаны во время выполнения. Кроме того, вам не нужно беспокоиться об обработке всей инициализации в одном конструкторе, используя эту технику.)
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!'
И, наконец, вы можете бросить все это вместе с простым объявлением node:
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()
Это должно дать вам представление об альтернативах системе жесткого типа. Обратите внимание, что вам не нужно полагаться на иерархию типов вообще - просто реализуйте (потенциально неявный) общий интерфейс.
LESS OLD:
Прочитав вопрос более внимательно, я вижу, что у вас есть классический пример наследование алмазов, который является зло, которое заставляет людей бежать в сторону одиночного наследования.
Вероятно, вы не хотите, чтобы это начиналось, поскольку иерархия классов означает приседание в Python. То, что вы хотите сделать, это сделать SensorInterface
(минимальные требования для датчика) и иметь кучу классов "mixin", которые имеют полностью независимую функциональность, которую можно вызвать с помощью методов разных имен. В области вашего сенсора вы не должны говорить такие вещи, как isinstance(sensor, PositionSensor)
- вы должны сказать такие вещи, как "может ли этот датчик геолокализовать?" в следующем виде:
def get_position(sensor):
try:
return sensor.geolocate()
except AttributeError:
return None
Это основа философии утиной печати и EAFP (проще просить прощения, чем разрешения), оба языка Python включает в себя.
Вероятно, вам следует описать, какие методы будут использовать эти датчики, чтобы мы могли описать, как вы можете использовать классы mixin для вашей архитектуры плагина.
OLD:
Если они пишут код в модуле, который попадает в пакет плагинов или что у вас есть, вы можете волшебным образом использовать классы для них при импорте своих модулей плагина. Что-то по строкам этого фрагмента (непроверено):
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
Вы можете найти модули в пакете с другой функцией.
Ответ 2
super
вызывает следующий класс в списке mro-list. Это работает, даже если вы оставите в форме __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!
>>>
Изменить: Переписал ответ. (Снова)
Ответ 3
Здесь частичное решение:
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
).
Изменить: Я разместил это, прежде чем увижу ваши обновления; Я перечитаю ваш вопрос и при необходимости обновить это решение.