Теоретически объект может служить контейнером для значений.

Ресурсы по преобразованию типов есть почти в каждом языке программирования. Это означает, что значение может быть преобразовано в другое значение, если оно совместимо.

Возьмем, к примеру:

n1 = 8
n2 = str(8)
print(type(n1))
print(type(n2))
//<class 'int'>
//<class 'str'>

Преобразование прошло успешно, потому что метод str () имеет ряд требований, которые необходимо выполнить, чтобы преобразовать число 8 в строку «8».

Теперь давайте посмотрим на другой пример:

n1 = 8
n2 = int('string')
//ValueError: invalid literal for int() with base 10: 'string'

Метод int () выдал ошибку, потому что int () имеет правило, согласно которому для преобразования текстового значения в число это значение должно представлять число.

Таким образом, примитивные типы могут обеспечить целостность преобразуемых данных.

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

По моим наблюдениям и предыдущим экспериментам найденный тип должен содержать:

  • Набор требований, которые необходимо выполнить, чтобы правильно присвоить значение типа.
  • Набор настраиваемых исключений, которые в дальнейшем можно использовать для обработки нестандартных значений.
  • Набор допустимых значений.
  • Простой способ чтения значения типа

Эксперимент:

В этом эксперименте я создам тип, который принимает только целые числа от 0 до 9.

Я начал с создания класса с именем singleDigitInt.

class singleDigitInt:

Проверка:

Мне нужен метод, который подтвердит значение:

def __validate(self):
    if type(self.value) != int: raise TypeError('the stored value is of incorrect type')
    if self.value > 9: raise numberTooHighError
    if self.value < 0: raise numberTooLowError

Пояснение к коду:

if type(self.value) != int: raise TypeError

Здесь я проверяю, имеет ли сохраненное значение тип int.

if value > 9: raise numberTooHighError
if value < 0: raise numberTooLowError

Он выдает одну ошибку, если число больше 9, и другую ошибку, если оно меньше 0.

Вы можете подумать: почему я не создал один тест для этих двух сценариев?

Это потому, что я хочу обрабатывать высокие значения иначе, чем низкие значения.

Наша проверка завершена. Но теперь возникает другая проблема: numberTooHighError и numberTooLowError не являются базовыми исключениями, это настраиваемые исключения.

Итак, давайте реализуем это:

class numberTooHighError(Exception):
    def __init__(self, message = 'the number is above 9!'):
        self.message = message;
def __repr__(self):
        print(self.message)
class numberTooLowError(Exception):
    def __init__(self, message = 'the number is below 0!'):
        self.message = message;
def __repr__(self):
        print(self.message)

Это довольно простые исключения, их не нужно усложнять, потому что сам факт наличия определенного исключения для случая может открыть двери для конкретного лечения позже в разных разделах кода.

Форматирование:

Теперь я могу реализовать форматирование разрешенных значений и крайних случаев:

def __format(self, value):
    if type(value) == singleDigitInt: return value()
    if type(value) == int: return value
    result = value
    if type(result) == list and len(result) == 1: result = int(result[0])
    if type(_value) == str and \\
        re.search('b', _value) and \\
        re.search('[2-9]', _value) == None: _value = int(re.sub('b', '', _value), 2)
    return int(result)

Пояснение к коду:

Раздел форматирования должен:

  • Проверьте, нуждается ли переданное значение в форматировании
  • Форматировать допустимые значения и крайние случаи

Проверка необходимости форматирования переданного значения:

Я выявил два случая, когда форматирование не требуется: когда переданное значение уже имеет тип singleDigitInt и если переданное значение уже является числом int:

if type(value) == singleDigitInt: return value()

Было отличной идеей проверить, имеет ли уже передаваемое значение тип singleDigitInt. Если да, значит, это значение уже прошло необходимую проверку и форматирование. Так что не нужно его снова форматировать.

if type(value) == int: return value

Если переданное значение является числом int, это означает, что оно уже находится в формате, который можно сохранить и проверить. Просто верни его.

Отформатируйте допустимые значения и крайние случаи:

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

Я думал о:

  • Длинный список из одного пункта
  • Двоичное число
result = value

Начнем с клонирования значения. Теперь я могу использовать измененное значение и исходное значение при необходимости.

if type(_value) == list and len(_value) == 1: _value = _value[0]

Затем я проверю, является ли клонированное значение списком, и если да, то есть ли в нем только один элемент.

Клонированное значение пока не будет преобразовано в int, потому что, если кто-то передаст этой функции массив с двоичным числом, мы хотим, чтобы это число было преобразовано в десятичное.

if type(_value) == str and \\
    re.search('^b', _value) and \\
    re.search('[2-9]', _value) == None: _value = int(re.sub('b', '', _value), 2)

Это наш первый «комплексный» тест.

Здесь я импортировал библиотеку регулярных выражений и использовал ее, чтобы проверить, состоит ли значение только из нулей и единиц, отбросив все значения с числами больше 1.

Позже я обнаружил ошибку, при которой строка «10» рассматривалась бы этой системой как 2. Поэтому вам нужно указать, является ли число двоичным, поставив перед числом букву b.

Таким образом, числа вроде 10, 11, 100, 101 или 111 не будут считаться двоичными, если вы явно этого не хотите.

return int(result)

Наконец, мы возвращаем значение, преобразованное в int. Если после форматирования число по-прежнему меньше 0 или больше 10, оно будет проверено методом __validate () позже.

Получение значения:

Есть много способов легко получить значение из объекта. Поскольку мы используем Python, мы будем использовать метод call.

def __call__(self, *args, **kwargs):
        return self.__value

В двух словах: метод call - это то, что происходит, когда экземпляр объекта вызывается как функция.

Если я сделаю это:

x = singleDigitInt(4)
print(x())

На выходе будет 4.

Готовый код:

Теперь мне просто нужно завершить функцию с помощью метода init:

def __init__(self, value):
        self.__value = self.__format(value);
        self.__validate()

И конечный результат будет:

import re
class singleDigitInt:
    def __init__(self, value):
        self.__value = self.__format(value);
        self.__validate()
    def __validate(self):
        if type(self.__value) != int: raise TypeError('the stored value is of incorrect type')
        if self.__value > 9: raise numberTooHighError
        if self.__value < 0: raise numberTooLowError
    def __format(self, value):
        if type(value) == singleDigitInt: return value()
        if type(value) == int: return value
        _value = value
        if type(_value) == list and len(_value) == 1: _value = _value[0]
        if type(_value) == str and \
                re.search('^b', _value) and \
                re.search('[2-9]', _value) == None: _value = int(re.sub('b', '', _value), 2)
        return int(_value)
    def __call__(self, *args, **kwargs):
        return self.__value
class numberTooHighError(Exception):
    def __init__(self, message = 'the number is too high!'):
        self.message = message;
    def __repr__(self):
        print(self.message)
class numberTooLowError(Exception):
    def __init__(self, message = 'the number is too low!'):
        self.message = message;
    def __repr__(self):
        print(self.message)

В этом контексте цель метода __init__ - определить, какие переменные будут использоваться во всем типе.

__values ​​должны быть закрытыми, потому что меньше всего нам нужно, чтобы кто-то создал экземпляр типа singleDigitInt и сразу перешел к number.value = 'some random text'.

Это разрушило бы все предназначение типа. Таким образом, вы не отправляете значение на проверку или форматирование.

Тесты:

def test_simpleNumber(self):
    result = singleDigitInt(4)
    self.assertEqual(result(), 4)

Этот тест прост. Ставлю класс 4, хранит 4. Так работает.

def test_arrayNumber(self):
    result = singleDigitInt([6])
    self.assertEqual(result(), 6)

Это допустимый сценарий. Он получает массив длиной 1 элемент и сохраняет 0-й элемент как число. Оно работает.

def test_binaryNumber(self):
    result = singleDigitInt('b1001')
    self.assertEqual(result(), 9)

Другой разрешенный сценарий. Он получает двоичное число, проверяет, является ли оно двоичным значением, а если да, преобразует его в десятичное число и сохраняет его. Также работает.

def test_testNumberTooHigh(self):
    with self.assertRaises(numberTooHighError):
        singleDigitInt(20)
try:
        result = singleDigitInt(20)
    except numberTooHighError:
        result = singleDigitInt(9)
    self.assertEqual(result(), 9)

Здесь мы проверяем, генерирует ли тип исключение, если число слишком велико. Если это так, результатом будет максимально возможное число. В этом случае: 9.

Допустим, у вас откуда-то поступают данные категоризации, и у этих категорий есть идентификаторы. Это число от 0 до 9, значение по умолчанию - 5.

Мы можем сделать это:

try:
    result = singleDigitInt(20)
except numberTooHighError:
    result = singleDigitInt(5)
self.assertEqual(result(), 5)

Мы можем сделать то же самое с числом ниже нуля:

def test_testNumberTooHigh(self):
    with self.assertRaises(numberTooLowError):
        singleDigitInt(-5)
try:
        result = singleDigitInt(-5)
    except numberTooLowError:
        result = singleDigitInt(0)
    self.assertEqual(result(), 0)

Если преобразование не удалось из-за слишком маленького числа, мы можем создать другой объект с наименьшим возможным номером.

Этот тип программирования зависит от конкретного случая, что означает, что класс сообщит вам, что пошло не так, в каких случаях проверка не удалась, и даст вам возможность исправить это на ходу.

Более надежный тест

Рассмотрим этот случай:

  • Я получаю массив чисел с сервера.
  • Эти данные имеют числа, которые отражают категорию.
  • На этом сервере есть устаревшие данные. Эти числа могут быть от -1 до 15.
  • Они могут быть строкой, а могут и не быть.
  • Недавно руководство сократило количество категорий до цифр от 0 до 9.
  • -1 категория - это ошибочные данные, и их не следует рассматривать.
  • числа выше 9 будут включены в категорию 9
  • Ох… и есть устаревшая часть… когда база данных создавалась еще в 1980 году, некоторые числа хранились в двоичном формате.
  • К счастью, специалист по бэкенду изменил все идентификаторы категорий, чтобы перед двоичными файлами у них была буква «b».

С моим недавно созданным типом я могу сделать что-то вроде этого:

def test_dataTest(self):
    dataFromDB = [0, -1, 5, 4, '11', 13, 15, '12', -1, 7, 14, '-1', -1, 2, 10, 'b01', 'b1000', 'b1111', 'b1010', 'b0100', '1']
    response = []
    for data in dataFromDB:
        try:
            newData = singleDigitInt(data)
        except numberTooHighError: newData = singleDigitInt(9)
        except numberTooLowError: continue
        response.append(newData())
    print(response)
# [0, 5, 4, 9, 9, 9, 9, 7, 9, 2, 9, 1, 8, 9, 9, 4, 1]

В заключении:

Итак, в заключение, то, что я наблюдал с помощью этого метода, было:

Возможность повторного использования:

Для начала я могу сказать, что если число слишком велико или слишком мало, это не произвольно или что-то, что есть только в этой функции.

Это часть типа singleDigitInt.

Везде, где я использую этот класс, он будет вызывать эти исключения, если указаны неправильные значения.

Это хорошо. Потому что он создает централизованный источник этого ценностного шаблона. Мне не нужно беспокоиться о том, что одна функция предоставляет действительные однозначные числа, а другая функция не предоставляет этого.

Кодовое сообщение:

Когда вы работаете в больших командах, вы найдете множество разных реализаций одного и того же.

Создание исключений для конкретных типов может сделать общение в вашей команде эффективным:

  • Это может помочь при проверке кода, поскольку разработчик, проверяющий код, может легко отслеживать поток данных внутри кода.
  • Это также может помочь в исправлении ошибок, поскольку вы можете легко определить действие, которое нужно предпринять, когда значение неверно, и сообщить об этом.

Код очистки:

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

И, создав метод для выполнения этих проверок, у нас нет подписи для значений, поступающих из этого метода. Поэтому каждый раз, когда мы хотим получить такое значение, нам придется запускать эту функцию снова.

Наличие подписи для такого типа значения не только полезно, но и делает ваш код более чистым.

Надеюсь, вам понравилась эта статья. Это мой первый и, надеюсь, первый из многих.

Спасибо за чтение!