Как ограничить атрибуты в методе класса?

В классе необходимо реализовать метод с проверкой введённых атрибутов. Например для метода scale нужно задать три параметра: тип датчика (гироскоп или акселерометр), ось (x, y, z) и заданное значение типа float или int в диапазоне [-1; 1]. Как правильно всё это проверить и в случае введении пользователем не верного значения вызвать ошибку? Я сделал это с помощью обычных проверок и вышло довольно объёмно с учётом того, что тут нет дальнейшей логики работы с датчиком. Может быть есть более красивый и правильный способ решения?

class Sensor:
    def scale(self, sensor_type, axis, value):
        if not (isinstance(sensor_type, str) or isinstance(axis, str) \ 
               or isinstance(value, (int, float))):
            raise Exception("Введён не верный тип данных")
        elif not (sensor_type == 'gyro' or sensor_type == 'accl'):
            raise Exception("Введён не верный тип датчика")
        elif not (axis == 'x' or axis == 'y' or axis == 'z'):
            raise Exception("Введён не верный тип оси")
        elif -1 < value > 1:
            raise Exception("Вышли за пределы допустимого интервала")
        if sensor_type == 'gyro':
            if axis == 'x':
                print('do something gyro x')
            elif axis == 'y':
                print('do something gyro y')
            elif axis == 'z':
                print('do something gyro z')
        elif sensor_type == 'accl':
            if axis == 'x':
                print('do something accl x')
            elif axis == 'y':
                print('do something accl y')
            elif axis == 'z':
                print('do something accl z')

Кроме этого, чтобы метод работал необходимо вызывать его так:

sensor.scale('gyro', 'z', 0)

Мне бы хотелось избавится от кавычек и делать вызов следующим образом:

sensor.scale(gyro, z, 0)

Но я, к сожалению, не нашёл информации как это реализовать.


Ответы (1 шт):

Автор решения: Roman Konoval

Идеальная ситуация, когда каждую функцию легко понять. Чтоб это можно было сделать, нужно чтобы выполнялись два условия:

  1. функция должна быть достаточно короткая
  2. все действия в ней должны быть на одном уровне абстракции

Проблема, которая у вас возникла, как раз этого рода - функция разрослась. И в ней смешаны сразу несколько уровней абстракции. В частности функция знает что делать ("нужно проверить что переменные правильные"), но также она знает как это делать.

Ситуацию можно сильно улучшить выделив функцию или функции для проверки параметров а также для действий, как раз чтобы разделить "что" и "как".

   def scale(self, sensor_type, axis, value):
        self.validate_scale_parameters(sensor_type, axis, value)
        if sensor_type == 'gyro':
            self.scale_gyro(axis, value)
        elif sensor_type == 'accl':
            self.scale_accl(axis, value)

   def validate_scale_parameters(self, sensor_type, axis, value):
        if not (isinstance(sensor_type, str) or isinstance(axis, str) \ 
               or isinstance(value, (int, float))):
            raise Exception("Введён не верный тип данных")
        elif not (sensor_type == 'gyro' or sensor_type == 'accl'):
            raise Exception("Введён не верный тип датчика")
        elif not (axis == 'x' or axis == 'y' or axis == 'z'):
            raise Exception("Введён не верный тип оси")
        elif not (-1 < value < 1):
            raise Exception("Вышли за пределы допустимого интервала")

   def scale_gyro(self, axis, value):
        if axis == 'x':
            print('do something gyro x')
        elif axis == 'y':
            print('do something gyro y')
        elif axis == 'z':
            print('do something gyro z')

   def scale_accl(self, axis, value):
        if axis == 'x':
            print('do something accl x')
        elif axis == 'y':
            print('do something accl y')
        elif axis == 'z':
            print('do something accl z')

Далее очень сильно поможет использование типизированных объектов вместо обобщенных. Т.е. enum-ов для перечислимых типов. Это уберет некоторые проверки вообще и сделает код в целом более поддерживаемым:

from enum import Enum
class SensorType(Enum):
    GYRO = 'gyro'
    ACCL = 'accl'

class Axis(Enum):
   X = 'x'
   Y = 'y'
   Z = 'z'


class Sensor:
   def validate_scale_parameters(self, sensor_type, axis, value):
        # теперь достаточно проверить только тип объекта
        if not (isinstance(sensor_type, SensorType) or isinstance(axis, Axis) \ 
               or isinstance(value, (int, float))):
            raise Exception("Введён не верный тип данных")
        elif not (-1 < value < 1):
            raise Exception("Вышли за пределы допустимого интервала")



# вызов выглядит так - почти то, что вы хотели. В определенном смысле даже лучше
sensor.scale(SensorType.GYRO, Axis.X, 0.5)

Для value имеет смысл то же самое сделать, ввести тип, который уже сам будет контролировать ограничения:

class SensorValue:
   def __init__(self, value):
     if not isinstance(value, (int, float)):
            raise Exception("Введён не верный тип данных")
     if not (-1 < value < 1):
      raise Exception("Вышли за пределы допустимого интервала")
     self._value = value

   @property
   def value(self):
     return self._value

Теперь опять же в scale достаточно контролировать что value имеет тип SensorValue:

   def scale(self, sensor_type, axis, value):
        # теперь достаточно проверить только тип объекта
        # отдельная функция не нужна, т.к. проверка уже упростилась до почти тривиальной
        if not (isinstance(sensor_type, SensorType) or isinstance(axis, Axis) \ 
               or isinstance(value, SensorValue)):
            raise Exception("Введён не верный тип данных")
        if sensor_type == SensorType.GYRO:
            self.scale_gyro(axis, value)
        elif sensor_type == SensorType.ACCL:
            self.scale_accl(axis, value)

   def scale_gyro(self, axis, value):
        if axis == Axis.X:
            print('do something gyro x')
            print(value.value)
        elif axis == Axis.Y:
            print('do something gyro y')
        elif axis == Axis.Z:
            print('do something gyro z')

Ну и вызов:

sensor.scale(SensorType.GYRO, Axis.X, SensorValue(0.5))

Далее можно избавится еще и от диспечеризации по типу сенсора используя полиморфизм, но это уже тема для другого вопроса и ответа.

→ Ссылка