Search code examples
pythonmatlabparsingpython-3.2

Learning Python Parsers


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

Solution

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