Home > Mobile >  Python init object of generic type
Python init object of generic type

Time:09-25

Coming from a C# background and knowing its generic type approaches I'm now trying to implement something similar in Python. I need to serialize and de-serialize classes in a special string format, so I created the following two base classes, the first for single entity serialization and the second one for list serialization of that entity type.

from typing import Any, TypeVar, List, cast, Type, Generic, NewType
import re

T = TypeVar('T')

class Serializable(Generic[T]):
    def to_str(self) -> str:
        raise NotImplementedError

    @classmethod
    def from_str(cls, str: str):
        raise NotImplementedError


class SerializableList(List[Serializable[T]]):
    def __init__(self):
        self.separator: str = "\n"

    @classmethod
    def from_str(cls, str: str):
        list = cls()
        for match in re.finditer(list.separator, str):
            list.append(T().from_str(match)) # <-- PROBLEM: HOW TO INIT A GENERIC ENTITY ???
            # list.append(Serializable[T].from_str(match)) <-- Uses base class (NotImplemented) instead of derived class
        
        return list
    
    def to_str(self) -> str:
        str = ""
        for e in self:
            str = str   f"{e.to_str()}{self.separator}"
    
        return str

Then I can derive from those classes and have to implement to_str and from_str. Please see the marker <-- PROBLEM". I have no idea how I can init a new entity of the currently used type for the list. How do we do this in the Python way?

CodePudding user response:

For now I found a dirty solution - this is to add a Type (constructor) parameter of the list entries like so:

class SerializableList(List[Serializable[T]]):
    #                                           This one
    #                                              |
    #                                              v
    def __init__(self, separator: str = "\n", entity_class: Type = None):
        self.separator = separator
        self.entity_class = entity_class


    @classmethod
    def from_str(cls, str: str):
        list = cls()
        for match in re.finditer(list.separator, str):
            list.append(list.entity_class.from_str(match))

        return list

I wonder if there is a cleaner way to get the correct [T] type constructor from List[T] since it is already provided there?

CodePudding user response:

As @user2357112supportsMonica says in the comments, typing.Generic is pretty much only there for static analysis, and has essentially no effect at runtime under nearly all circumstances. From the look of your code, it looks like what you're doing might be better suited to Abstract Base Classes (documentation here, tutorial here), which can be easily combined with Generic.

A class that has ABCMeta as its metaclass is marked as an Abstract Base Class (ABC). A subclass of an ABC cannot be instantiated unless all methods in the ABC marked with the @abstractmethod decorator have been overridden. In my suggested code below, I've explicitly added the ABCMeta metaclass to your Serializable class, and implicitly added it to your SerializableList class by having it inherit from collections.UserList instead of typing.List. (collections.UserList already has ABCMeta as its metaclass.)

Using ABCs, you could define some interfaces like this (you won't be able to instantiate these because of the abstract methods):

### ABSTRACT INTERFACES ###

from abc import ABCMeta, abstractmethod
from typing import Any, TypeVar, Type, Generic
from collections import UserList
import re

T = TypeVar('T')

class AbstractSerializable(metaclass=ABCMeta):
    @abstractmethod
    def to_str(self) -> str: ...

    @classmethod
    @abstractmethod
    def from_str(cls: Type[T], string: str) -> T: ...


S = TypeVar('S', bound=AbstractSerializable[Any])


class AbstractSerializableList(UserList[S]):
    separator = '\n'

    @classmethod
    @property
    @abstractmethod
    def element_cls(cls) -> Type[S]: ...

    @classmethod
    def from_str(cls, string: str):
        new_list = cls()
        for match in re.finditer(cls.separator, string):
            new_list.append(cls.element_cls.from_str(match))
        return new_list
    
    def to_str(self) -> str:
        return self.separator.join(e.to_str() for e in self)

You could then provide some concrete implementations like this:

class ConcreteSerializable(AbstractSerializable):
    def to_str(self) -> str:
        # put your actual implementation here

    @classmethod
    def from_str(cls: Type[T], string: str) -> T:
        # put your actual implementation here

class ConcreteSerializableList(AbstractSerializableList[ConcreteSerializable]:
    # this overrides the abstract classmethod-property in the base class
    element_cls = ConcreteSerializable

(By the way — I changed several of your variable names — str, list, etc — as they were shadowing builtin types and/or functions. This can often lead to annoying bugs, and even if it doesn't, is quite confusing for other people reading your code! I also cleaned up your to_str method, which can be simplified to a one-liner, and moved your separator variable to be a class variable, since it appears to be the same for all class instances and does not appear to ever be altered.)

  • Related