Reputation: 639
I need to write an abstract class which will act like an driver to "something external".
# ./base.py
import typing as t
from abc import ABC, abstractmethod
class DefaultClass: pass
MyType = t.TypeVar("MyType", bound=DefaultClass)
class Driver(ABC):
def __init__(self, return_class: t.Type[MyType]):
self.return_class = return_class
def driver_call(self, param: str) -> MyType:
return self._driver_call(param)
@abstractmethod
def _driver_call(self, param: str) -> MyType:
raise NotImplementedError()
But here I struggle. Let me try to inherit from this class:
# ./mydriver.py
from base import Driver, DefaultClass
class CustomDriver(Driver):
def __init__(self, return_class):
super().__init__(return_class)
def _driver_call(self, param: str) -> ???:
return self.return_class(param)
# Usage
if __name__ == "__main__":
class CustomClass(DefaultClass):
def __init__(self, name: str):
self.name = name
cd = CustomDriver(CustomClass)
instance = cd.driver_call("some_string")
I struggle with next point:
_driver_call()
to make it clear that it should return instance of class which is set in self.return_class
I've read about generics type etc, but currently I can't map info what i read with my goal to make "interface" for driver.
OR may be I am so wrong that there is absolutely another way to do it?
Upvotes: 0
Views: 120
Reputation: 18548
You did not give enough context to provide an unambiguous recommendation, but I'll give it a shot.
If that "something external" being driven by your Driver
subclasses indeed always inherits from the same base class, there is no need for typing.Protocol
here since we have nominal subtyping to guide us.
I agree with @joel that this screams typing.Generic
and that _driver_call
is redundant (as it appears from your example right now).
The nice thing about abstract base classes is that you typically don't need to write an __init__
method. You can just lay out the attributes and annotate them with their expected types.
I am assuming you know the concrete CustomClass
you want to drive with a Driver
subclass in advance. A also assume that not all subclasses of DefaultClass
will have the same constructor interface, so your abstract Driver
cannot mirror the constructor's signature.
from abc import ABC, abstractmethod
from typing import Any, Generic, TypeVar
T = TypeVar("T", bound="DefaultClass")
# External:
class DefaultClass:
pass
# Generic abstract base class:
class Driver(ABC, Generic[T]):
return_class: type[T]
@abstractmethod
def driver_call(self, *args: Any, **kwargs: Any) -> T:
...
# External, but with a specific init signature:
class CustomClass(DefaultClass):
def __init__(self, name: str) -> None:
self.name = name
# Specification:
class CustomDriver(Driver[CustomClass]):
def __init__(self, return_class: type[CustomClass]) -> None:
self.return_class = return_class
def driver_call(self, param: str) -> CustomClass:
return self.return_class(param)
If you want your CustomDriver
to also be generic over T
, you'll need to be careful with the constructor signature because it seems you assume you'll always be able to initialize a DefaultClass
subtype with just a str
argument. Maybe that is what @joel meant with his comment about typing it with Callable
. In that case it would look more like this:
from abc import ABC, abstractmethod
from collections.abc import Callable
from typing import Any, Generic, TypeVar
T = TypeVar("T", bound="DefaultClass")
class DefaultClass:
pass
class Driver(ABC, Generic[T]):
@abstractmethod
def driver_call(self, *args: Any, **kwargs: Any) -> T:
...
class CustomDriver(Driver[T]):
def __init__(self, cls_constructor: Callable[[str], T]) -> None:
self.cls_constructor = cls_constructor
def driver_call(self, param: str) -> T:
return self.cls_constructor(param)
class CustomClass(DefaultClass):
def __init__(self, name: str) -> None:
self.name = name
Both version work and are type safe with this usage:
cd = CustomDriver(CustomClass)
instance = cd.driver_call("some_string")
A class is also a callable returning an instance of itself, which is why the Callable[..., T]
notation is technically more general than type[T]
.
As you noticed, there are a lot of assumptions here. I hope you see that you need to be clearer in your questions, so that others need to do less guessing about what it is you actually want. If you care to elaborate further and provide more context in your original post, I can try and amend my answer accordingly.
Upvotes: 1