Learning Python Parsers

68 Views Asked by At

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
1

There are 1 best solutions below

1
Brian61354270 On BEST ANSWER

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 Platforms 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.