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

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

Первый уровень защиты состоит в проверке непротиворечивости входящих данных и их фактической достоверности. Мы не хотим кормить нашу систему атаками SQL-инъекций или даже неполными полями, которые не могут быть правильно обработаны. Распространенным и классическим подходом к защите информационных систем является использование валидации данных: мы выполняем проверки на валидность и, если они не все проходят, то отбрасываем поступающие данные.

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

Простой пример

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

Самый простой способ смоделировать данные в Python — использовать словарь:

customer1 = {
    "name": "John", 
    "surname": "Smith",
    "email": "[email protected]",
}
customer2 = {
    "name": "Jessica",
    "surname": "Allyson",
    "telephone": "0123456789",
}

Этот подход следует идее, подчеркнутой Ричем Хики в его знаменитом выступлении «Может быть, и нет». В этом выступлении изобретатель Clojure защищает идею использования словарей только с реально существующими полями, а не с использованием необязательных полей (называемых Maybe на языках, которые он упоминает).

Рич Хикки — харизматичный и высококвалифицированный инженер и общественный деятель, и его речи легко убедить. Однако у этого моделирования данных есть один существенный недостаток: после проверки структуры словаря код, который приходит после проверки, не будет иметь дополнительной информации о внутренней структуре данных.

Поясним на примере:

def validate_customer_data(customer: Dict[str, str]) -> None:
    if "name" not in customer or "surname" not in customer:
        raise ValueError("Customer data must contain keys name and surname")
    if "email" not in customer and "telephone" not in custormer):
        raise ValueError("At least one field among email and telephone must be present in customer data")
def contact_customer(customer: Dict[str, str]) -> None:    
    if "telephone" in customer:
        open_call(customer["telephone"])
    elif "email" in customer:
        open_email_client(customer["email"])

customer = receive_customer_data()
validate_customer_data()
contact_customer(customer)

В приведенном выше фрагменте кода мы сначала получаем данные клиента каким-то образом, затем проверяем эти данные и, наконец, используем их для связи с клиентом. Однако после проверки мы знаем только то, что клиент предоставил нам хотя бы один адрес электронной почты или номер телефона, но не какой из двух. Услуга, которую мы получаем от функции проверки, заключается в том, чтобы вызвать ошибку, если ни одно из двух полей не присутствует. Функция contact_customer должна контролировать, какое из двух полей присутствует, и действовать соответствующим образом.

Обратите внимание, что подход к моделированию, основанный на классе (данных), заставит нас иметь дело только с объектами None и здесь мало поможет:

from dataclasses import dataclass
@dataclass
class Customer:
    name: str
    surname: str
    email: Optional[str]
    telephone: Optional[str]
    def contact(self) -> None:
       if self.telephone is not None:
           open_call(self.telephone)
       elif self.email is not None:
           open_email_client(self.email)

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

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

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



Разобрать, не проверять

Разбирать, не проверять — это концепция, введенная Алексис Кинг в ее одноименной статье, посвященной языку Haskell. Однако большие различия между Haskell и Python не должны заставлять вас думать, что одни и те же идеи нельзя перенести в Python. По сути, синтаксический анализ — это идея, лежащая в основе библиотеки Pydantic, с которой я предлагаю вам ознакомиться после прочтения этой статьи. Статья Кинга также должна быть вполне доступной даже для тех, кто не знает Haskell, как только идея будет прояснена.

Основная идея заключается в том, что валидация предотвращает попадание неверных данных в наше приложение. Однако некоторая информация создается в процессе проверки, а затем сразу же отбрасывается. С другой стороны, синтаксический анализ позволяет нам по-прежнему отклонять неправильно сформированные данные, а также сопоставлять допустимые данные с типами данных, которые сохраняют такую ​​информацию. Такие типы будут использоваться остальной частью вашего приложения для предотвращения уже выполненных элементов управления.

Например, в приведенном выше примере словари могут быть сопоставлены с новыми типами:

@dataclass
class CustomerWithPhone:
  name: str
  surname: str
  phone: str
@dataclass
class CustomerWithEmail:
  class CustomerWithPhone:
  name: str
  surname: str
  email: str
@dataclass
class CustomerWithPhoneAndEmail:
  class CustomerWithPhone:
  name: str
  surname: str
  phone: str
  email: str

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

from functools import singledispatch

@singledispatch
def contact_customer(customer):
  raise NotImplementedError("Cannot contact a customer of unknown type")
@contact_customer.register(CustomerWithPhone)
def _contact_customer_by_phone(customer):  # this name is irrelevant 
  .
  .
  call_number(customer.phone)
  .
  .
@contact_customer.register(CustomerWithEmail)
def _contact_customer_by_email(customer):  # this name is irrelevant 
  .
  .
  send_email(customer.email)
  .
  .

Приведенный выше код объявляет универсальную функцию с именем contact_customer и определяет для нее две разные реализации, соответственно, для двух типов ввода CustomerWithPhone и CustomerWithEmail. Интерпретатор Python во время выполнения решает, какую функцию вызывать, в зависимости от типа объекта во время выполнения. С вашей стороны не нужно писать какой-либо код, чтобы определить правильную функцию для вызова.

Чтобы получить максимальную пользу от наших определенных типов, становится важным использовать статические проверки типов, например, с mypy, чтобы вы могли иметь автоматический контроль правильности кода, который является как статическим, так и динамическим. Здесь важно то, что это не ручное управление, и они не состоят из кода, который вы (или ваша команда) должны поддерживать.

Использование статической типизации для работы с проанализированными данными — это способ обеспечить корректность кода. Вы никогда не будете вызывать неправильные функции или читать неправильные атрибуты. Mypy проверит и выделит такие ошибки для вас. И мы также получаем бесплатную поддержку IDE с точки зрения автозаполнения и предложений переменных.

Заключение

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

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







Среднее членство

Вам нравится то, что я пишу, и вы подумываете о подписке на Medium Membership, чтобы иметь неограниченный доступ к статьям?

Если вы подпишитесь по этой ссылке, вы поддержите меня своей подпиской без каких-либо дополнительных затрат для вас https://medium.com/@mattiadigangi/membership