Как проверить атрибут класса в Python? - PullRequest
2 голосов
/ 03 октября 2019

Я пытался проверить классы, которые пользователи могут создавать в настройках стиля фреймворка. Я могу гарантировать, что атрибут класса присутствует в дочерних классах следующим образом:

from abc import ABC, abstractmethod

class A(ABC):
    @property
    @classmethod
    @abstractmethod
    def s(self):
        raise NotImplementedError

class ClassFromA(A):
    pass


ClassFromA()

Что приводит к следующему Exception:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: Can't instantiate abstract class ClassFromA with abstract methods s

Я также могу проверить типатрибута класса s во время создания класса с помощью декоратора, например, так:

from abc import ABC, abstractmethod

def validate_class_s(cls):
    if not isinstance(cls.s, int):
        raise ValueError("S NOT INT!!!")
    return cls

class A(ABC):
    @property
    @classmethod
    @abstractmethod
    def s(self):
        raise NotImplementedError

@validate_class_s
class ClassFromA(A):
    s = 'a string'

В результате:

Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
  File "<stdin>", line 3, in validate_class_s
ValueError: S NOT INT!!!

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

Есть ли способ проверки атрибута класса (s в примерах) в базовом классе? Желательно не слишком многословно?

Ответы [ 2 ]

2 голосов
/ 03 октября 2019

Вы можете использовать новую функцию в Python 3.6 __init_subclass__.
Это метод класса, определенный в вашем базовом классе, который будет вызываться один раз для каждого создаваемого подкласса во время создания. Для большинства утверждений прецедентов это может быть более полезным, чем ABC Python, который вызовет ошибку только во время создания экземпляра класса (и, наоборот, если вы хотите создать подкласс для других классов абстракции до того, как доберетесь до конкретного класса, вы должны будете проверить это на своемcode).

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

_sentinel = type("_", (), {})

class Base:
    def __init_subclass__(cls, **kwargs):
        errors = []
        for attr_name, type_ in cls.__annotations__.items():
            if not isinstance(getattr(cls, attr_name, _sentinel), type_):
                errors.append((attr_name, type))
        if errors:
            raise TypeError(f"Class {cls.__name__} failed to initialize the following attributes: {errors}")
        super().__init_subclass__(**kwargs)

    s: int


class B(Base):
    pass

Вы можете поставить collections.abc.Callable для аннотации для запрашивания методов или кортеж типа (type(None), int) для необязательного целого числа, но, к сожалению, isinstance не будет работать с универсальной семантикой, предоставляемой модулем "typing". Если вы хотите этого, я предлагаю взглянуть на проект pydantic и использовать его.

1 голос
/ 04 октября 2019

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

def validate_with(baseclass):
    def validator(cls):
        for n, t in baseclass.__annotations__.items():
            if not isinstance(getattr(cls, n), t):
                raise ValueError(f"{n} is not of type {t}!!!")
        return cls
    return validator


class BaseClass:
    s: str
    i: int


@validate_with(BaseClass)
class SubClass(BaseClass):
    i = 3
    s = 'xyz'

Он поднимает ValueError, если тип не соответствует, и AttributeError, если атрибут отсутствует.

Конечно, выможет собрать ошибки (как в предыдущем ответе) и представить их все за один раз вместо остановки при первой ошибке

...