简体   繁体   中英

How to create a Python ABC interface pattern using Pydantic

I'm implementing a Python Interface using the abstract base class (known as the strategy pattern). I want to be able to do this with Pydantic.

Without Pydantic, I would use properties, like this:

from abc import ABC,  abstractproperty

@dataclass
class PersonEntity(ABC):

    @abstractproperty
    def first_name(self):
        raise NotImplementedError

    @abstractproperty
    def last_name(self):
        raise NotImplementedError


@dataclass
class Person(PersonEntity):

    @property
    def first_name(self):
        return 'Jimmy'

    @property
    def last_name(self):
        return 'Kelly'

This way, if I were to implement another class, like

@dataclass
class SillyPerson(PersonEntity):

    @property
    def first_name(self):
        return 'Jimmy'

    @property
    def last_name(self):
        return 'Kelly'

    @property
    def sillyness(self):
        return 5

then the interface will throw an error. This helps constrain any new class that inherits from PersonEntity .

However, I want to spit this information into a FastAPI response object. I cannot do this without figuring out some kind of serializer to grab all the values of each property field, and just the property fields (which I'm struggling to do).

I would rather use Pydantic. In this case, I dont need properties, I can simply do:

from pydantic import BaseModel


class PersonEntity(ABC, BaseModel):
    first_name: str
    last_name: str


class Person(PersonEntity):
    first_name: str
    last_name: str

These will serialize in the way that I need, but I lose the interface functionality because now I have no properties, and therefore cannot use @abstractproperty .

So if I were to implement

class SillyPerson(PersonEntity):
    first_name: str
    last_name: str
    sillyness: str

there's no error, because pydantic allows this.

(Incidentally, I wasn't sure in these examples whether to inherit from BaseModel in child classes or not.)

Is there some way I can constrain the Pydantic model to give me the interface behaviour that I need, throwing errors when a field is introduced that is not included in the ABC PersonEntity class?

In your pydantic BaseModel definition, you can configure it to forbid extra parameters.

from pydantic import BaseModel, Extra
class PersonEntity(BaseModel):
    class Config:
        extra = Extra.forbid
    first_name: str
    last_name: str

Then constructing an instance of this with extra args:

sillyperson = PersonEntity(first_name='foo', last_name='bar', sillyness='zar')

Throws an error:

pydantic.error_wrappers.ValidationError: 1 validation error for PersonEntity
sillyness
  extra fields not permitted (type=value_error.extra)

What you currently have implemented will not work since

class SillyPerson(PersonEntity):
    first_name: str
    last_name: str
    sillyness: str

defines a new dataclass (it inherits from BaseModel) with a field called 'sillyness'. It will therefore do the opposite of you want; throwing an error if an attribute called sillyness is not provided.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM