Эту задачу подготовила подписчица Selectel Newsfeed Кристина Шараева специально для Академии Selectel. Решение написал наш коллега Никита Моторный — руководитель команды клиентских сервисов.
Условие
Python-разработчик Вася устроился в фармацевтическую компанию. Его первый проект связан с обработкой данных, которые в будущем могут быть использованы при разработке лекарств.
Однажды Вася заметил проблему в небольшом фрагменте кода, отвечающем за валидацию данных. Оказалось, что другие разработчики при определении новой _validate_* функции иногда забывали добавить ее в список required_checks внутри функции validate_data, из-за чего новые проверки не применялись.
Вася подумал, что можно использовать знания Python и уменьшить в коде вероятность ошибок, сделанных по невнимательности.
Задача
Помогите Васе исправить код:
import pandas as pd
def _validate_size(df: pd.DataFrame) -> bool:
return not df.empty
def _validate_columns(df: pd.DataFrame) -> bool:
required_columns = {
'smiles',
'molecule_name',
}
return required_columns.issubset(set(df.columns))
def …
_validate_molecule_name(df: pd.DataFrame) -> bool:
return not df['molecule_name'].isna().any()
# many other _validate_* functions
def validate_data(df: pd.DataFrame) -> bool:
required_checks = (
_validate_size,
_validate_columns,
_validate_molecule_name,
)
for check in required_checks:
if not check(df):
return False
return True
Решение
Код выше работает, хотя и нуждается в форматировании. Но если хочется локализовать добавление валидатора поближе к определению, можно сделать с помощью декоратора так:
import pandas as pd
from typing import Callable
class ValidatorsCollection:
validators = {}
@classmethod
def add_validator(cls, check_func: Callable):
cls.validators[check_func.__name__] = check_func
@classmethod
def validate_data(cls, df: pd.DataFrame) -> list:
errors = []
for name, check in cls.validators.items():
if not check(df):
errors.append(name)
return errors
molecules_df_validator = ValidatorsCollection.add_validator
@molecules_df_validator
def _validate_size(df: pd.DataFrame) -> bool:
return not df.empty
@molecules_df_validator
def _validate_columns(df: pd.DataFrame) -> bool:
required_columns = {
'smiles',
'molecule_name',
}
return required_columns.issubset(set(df.columns))
@molecules_df_validator
def _validate_molecule_name(df: pd.DataFrame) -> bool:
return 'molecule_name' in df and not df['molecule_name'].isna().any()
resp = ValidatorsCollection.validate_data(pd.DataFrame(["1"]))
print(resp)
А если хочется использовать магию метаклассов, можно сделать так:
from abc import ABCMeta, abstractmethod
import pandas as pd
validators = []
class ValidatorsCollcetionMeta(ABCMeta):
def __init__(cls, name, bases, namespace):
super().__init__(name, bases, namespace)
validators.append(cls)
def __str__(cls):
return cls.__name__
class Validator(metaclass=ValidatorsCollcetionMeta):
@staticmethod
@abstractmethod
def validate(df: pd.DataFrame) -> bool:
return True
class SizeValidator(Validator):
@staticmethod
def validate(df: pd.DataFrame) -> bool:
return not df.empty
class ColumnsValidator(Validator):
@staticmethod
def validate(df: pd.DataFrame) -> bool:
required_columns = {
'smiles',
'molecule_name',
}
return required_columns.issubset(set(df.columns))
class MoleculeNameValidator(Validator):
@staticmethod
def validate(df: pd.DataFrame) -> bool:
return 'molecule_name' in df and not df['molecule_name'].isna().any()
errors = []
for check in validators:
if not check.validate(pd.DataFrame()):
errors.append(str(check))
print(errors)
В обоих примерах мы немного изменили подход к валидации: убрали их взаимозависимость в порядке исполнения и добавили индикацию, какой валидатор не отработал. Эти два пункта позволили сразу запускать весь набор валидаторов и доставлять пользователю полный список ошибок при единственном запуске валидации.