I'm fairly new to Python and trying to set up a class with a constructor to have a small number of required properties and a larger number of optional ones with defaults and definitions of acceptable inputs.
I've tried using the argparse module, but I am not understanding how to parse the arguments then pass the results into the properties of the class. This also has not allowed me to define logical criteria for expected inputs.
I'm looking to do something similar to this MATLAB script.
methods
function obj = Platform(ClassID,varargin)
inPar = inputParser;
expectedClass = {'Ownship', 'Wingman', 'Flight Group', 'Unknown', 'Suspect', 'Neutral', 'Friend', 'Foe'};
validClassID = @(x) any(validatestring(x,expectedClass));
addRequired(inPar,'ClassID',validClassID)
defaultDim = struct('length', 0, 'width', 0, 'height', 0, 'oOffset', [0 0 0]);
validDim = @(x) ~isempty(intersect(fieldnames(x),fieldnames(defaultDim)));
addOptional(inPar,'Dimensions',defaultDim,validDim)
defaultPos = [0 0 0];
validPos = @(x) isclass(x,'double') && mean(size(x) == [1 3]);
addOptional(inPar,'Position',defaultPos,validPos)
defaultOr = [0 0 0];
validOr = @(x) isclass(x,'double') && mean(size(x) == [1 3]);
addOptional(inPar,'Orientation',defaultOr,validOr)
defaultTraj = struct('Waypoints',[0 0 0],...
'TimeofArrival',0,...
'Velocity',[0 0 0],...
'Orientation',[0 0 0]);
validTraj = @(x) ~isempty(fieldnames(x),fieldnames(defaultTraj));
addOptional(inPar,'Trajectory',defaultTraj,validTraj)
expectedDL = {'One','Two','Three};
defaultDL = {};
validDL = @(x) any(validatestring(x,expectedDL));
addOptional(inPar,'DataLinks',defaultDL,validDL)
defaultSens = {};
validSens = @(x) isa(x,'Sensor');
addOptional(inPar,'Sensors',defaultSens,validSens)
parse(inPar,ClassID,varargin{:})
obj.PlatformID = randi([1 10000]);
obj.ClassID = inPar.Results.ClassID;
obj.Dimensions = inPar.Results.Dimensions;
obj.Position = inPar.Results.Position;
obj.Orientation = inPar.Results.Orientation;
obj.Trajectory = inPar.Results.Trajectory;
obj.Sensors = inPar.Results.Sensors;
obj.DataLinks = inPar.Results.DataLinks;
end
Happily, Python has no need of doing this sort of ad-hoc string and array parsing.
Good Python code is object oriented. Instead of passing values around as raw strings and arrays, you should encapsulate them into objects of meaningful types. Those objects should be left responsible for validating themselves when constructed and for maintaining their invariants throughout their lifetime.
Even better Python code can take advantage of static type hinting to offload much of that validation to before your code is even run.
An idiomatic Python translation might look something like this (with some liberal guesswork interpretation):
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from typing import NamedTuple, Literal
class Position(NamedTuple):
x: float
y: float
z: float
@classmethod
def origin(cls) -> Position:
return cls(0, 0, 0)
class Orientation(NamedTuple):
yaw: float
pitch: float
roll: float
@classmethod
def pos_x(cls) -> Orientation:
return cls(0, 0, 0)
@classmethod
def pos_y(cls) -> Orientation:
return cls(1, 0, 0)
@classmethod
def pos_z(cls) -> Orientation:
return cls(0, 1, 0)
class Geometry(NamedTuple):
extent: Position
o_offset: Position
@classmethod
def unit_cube(cls) -> Geometry:
return cls((1, 1, 1), (0, 0, 0))
@dataclass
class Trajectory:
waypoints: list[Position] = field(default_factory=list)
time_of_arrival: float = 0
velocity: Position = Position.origin()
orientation: Orientation = Orientation.pos_x()
class Platform(ABC):
_geometry: Geometry
_position: Position
_orientation: Orientation
_trajectory: Trajectory
_datalinks: list[Literal['One','Two','Three']]
_sensors: list[Sensors]
def __init__(
self,
geometry: Geometry = Geometry.unit_cube(),
pos: Position = Position.origin(),
orientation: Orientation = Orientation.pos_x(),
trajectory: Trajectory | None = None,
datalinks: list[Literal['One','Two','Three'] | None = None,
sensors: list[Sensors] | None = None,
) -> None:
if trajectory is None:
trajectory = Trajectory()
if datalinks is None:
datalinks = []
if sensors is None:
sensors = []
self._geometry = geometry
self._position = pos
self._orientation = orientation
self._trajectory = trajectory
self._datalinks = datalinks
self._sensors = sensors
@abstractmethod
def do_something_class_specific(self) -> None:
...
class NeutralPlatform(Platform):
def do_something_class_specific(self) -> None:
self.watch_and_wait()
class FooPlatform(Platform):
def do_something_class_specific(self) -> None:
self.attack_mode()
That's it! Any Platform
s you construct will be fully validated, provided type checking passes. No need implement manual validation for things that the type system can already verify!
Need more invariants? Enforce them in the appropriate type. In a good object oriented design, Platform
does not (and should not) need to know anything about what makes a valid Orientation
, only that it has one and that it's already valid.