Python: проверка правильности ввода при изменении класса данных - PullRequest
0 голосов
/ 02 февраля 2019

В Python 3.7 есть эти новые контейнеры класса данных, которые в основном похожи на изменяемые именованные кортежи.Предположим, я создаю класс данных, предназначенный для представления человека.Я могу добавить проверку ввода с помощью функции __post_init__() следующим образом:

@dataclass
class Person:
    name: str
    age: float

    def __post_init__(self):
        if type(self.name) is not str:
            raise TypeError("Field 'name' must be of type 'str'.")
        self.age = float(self.age)
        if self.age < 0:
            raise ValueError("Field 'age' cannot be negative.")

Это позволит получать хорошие входные данные через:

someone = Person(name="John Doe", age=30)
print(someone)

Person(name='John Doe', age=30.0)

В то время как все эти неправильные входные данные приведут к ошибке:

someone = Person(name=["John Doe"], age=30)
someone = Person(name="John Doe", age="thirty")
someone = Person(name="John Doe", age=-30)

Однако, поскольку классы данных являются изменяемыми, я могу сделать это:

someone = Person(name="John Doe", age=30)
someone.age = -30
print(someone)

Person(name='John Doe', age=-30)

Таким образом, минуя проверку ввода.

Итак, каков наилучший способубедитесь, что поля класса данных не видоизменены во что-то плохое, после инициализации?

Ответы [ 2 ]

0 голосов
/ 02 февраля 2019

Классы данных - это механизм, обеспечивающий инициализацию по умолчанию для принятия атрибутов в качестве параметров, и хорошее представление, а также некоторые тонкости, такие как __post_init__ hook.

К счастью, они не связываются с любым другим механизмомдля доступа к атрибутам в Python - и вы по-прежнему можете создавать свои атрибуты dataclassess как property дескрипторы или как пользовательский класс дескриптора, если хотите.Таким образом, любой доступ к атрибутам будет проходить через функции получения и установки автоматически.

Единственный недостаток использования встроенного по умолчанию property состоит в том, что вы должны использовать его «по-старому»,а не с синтаксисом декоратора - это позволяет вам создавать аннотации для ваших атрибутов.

Итак, «дескрипторы» - это специальные объекты, назначенные атрибутам класса в Python таким образом, что любой доступ к этому атрибуту будет вызывать дескрипторы__get__, __set__ или __del__ методы.Встроенный property является условием для создания дескриптора, переданного от 1 до 3 функций, которые будут вызываться из этих методов.

Итак, без пользовательского дескриптора вы можете сделать:

@dataclass
class MyClass:
   def setname(self, value):
       if not isinstance(value, str):
           raise TypeError(...)
       self.__dict__["name"] = value
   def getname(self):
       return self.__dict__.get("name")
   name: str = property(getname, setname)
   # optionally, you can delete the getter and setter from the class body:
   del setname, getname

Используя этот подход, вы должны будете написать доступ к каждому атрибуту как два метода / функции, но вам больше не нужно будет писать ваш __post_init__: каждый атрибут будет проверяться сам.

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

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

def positive_validator(name, value):
    if value <= 0:
        raise ValueError(f"values for {name!r}  have to be positive")

class MyAttr:
     def __init__(self, type, validators=()):
          self.type = type
          self.validators = validators

     def __set_name__(self, owner, name):
          self.name = name

     def __get__(self, instance, owner):
          if not instance: return self
          return instance.__dict__[self.name]

     def __delete__(self, instance):
          del instance.__dict__[self.name]

     def __set__(self, instance, value):
          if not isinstance(value, self.type):
                raise TypeError(f"{self.name!r} values must be of type {self.type!r}")
          for validator in self.validators:
               validator(self.name, value)
          instance.__dict__[self.name] = value

#And now

@dataclass
class Person:
    name: str = MyAttr(str)
    age: float = MyAttr((int, float), [positive_validator,])

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

Обратите внимание, что вы можете легко добавить множество других проверок и преобразований для каждого из ваших атрибутов - и код самого __set_name__ можно изменить так, чтобы он автоматически анализировал __annotations__ в классе owner и автоматическиобратите внимание на типы - чтобы параметр типа не был необходим для самого класса MyAttr.Но, как я уже говорил, вы можете сделать это настолько изощренно, насколько захотите.

0 голосов
/ 02 февраля 2019

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

...