Search code examples
pythonpython-typing

Is there a method to check Generic Type on instancing time in pyhon?


I suppose how it can be check what type specified on Generic types in __init__ ?

On python 3.10, it could not be.

On first, I found this page: Instantiate a type that is a TypeVar

and make some utility for get __orig_class__.

And test on instancing time, there is no __orig_class__ attribute in that timing on self.

Then I searched again I found this issue page: https://github.com/python/typing/issues/658.

I recognized that any python might have this limit.

Is there any other way?

The following is sample code (not original one). This code cannot be executed for the reasons stated above. (I haven't run it, so there may be some bugs too.)

from abc import ABC, abstractmethod
from typing import Iterable, Any, TypeVar, Generic

class BaseLoader(ABC):
    """
        `BaseLoader` loads something and return some expected object.
        - load a file from path(str)
        - convert a raw data to some expected data (list like)
        - implement `load` in inherited class. 
    """
    def __init__(self, path: str):
        self._path = path

    @abstractmethod
    def load(self) -> Iterable:
        pass
    
class SimpleLineLoader(BaseLoader):
    """
        `SimpleTextBaseLoader` loads text data and return some converted lines.
        - load a utf-8 text file.
        - convert to a list of text lines.
        - implement `_convert_line` in inherited class. 
    """
    def load(self):
        with open(self._path, "r") as f:
            lines = f.readlines()
            converted_lines = [self._convert_line(line) for line in lines]
        return converted_lines
    
    @abstractmethod
    def _convert_line(self, line: str) -> Any:
        """
            `_convert_line` convert line string to another one. 
        """
        pass
        
    
class RawLineLoader(SimpleLineLoader):
    """
        `RawLineLoader` loads raw data. nothing to do.
    """
    def _convert_line(self, line):
        """
            nothing to do. just return raw value.
        """
        return line
    
class ElementTrimmedLineLoader(SimpleLineLoader):
    """
        `ElementTrimmedLineLoader` loads lines and regards each line as comma(,) separated data.
        it trims each tokens in the line and get back to the line.
        - trims each element separated with comma in each line.
        - sample:
        >>> from tempfile import TemporaryDirectory
        >>> data = ["a,  b,  d   ", "d  , e  ,  f"]
        >>> with TemporaryDirectory() as dname:
        >>>     with open(dname, "w") as f:
        >>>         f.writelines(data)
        >>>     loader = ElementTrimmedLineLoader(f.name)
        >>>     print(loader.load())
        [['a', 'b', 'd'], ['d', 'e', 'f']]
    """
    def __init__(self, path):
        super().__init__(path)
        self._delim = ","
        
    def _convert_line(self, line):
        """
            split with the delim and trim each element, 
            combine them to one with the delim again. 
        """
        delim = self._delim 
        converted = delim.join(elem.strip(' ') for elem in line.split(delim))
        return converted

    def get_delim(self):
        """
            get a delimiter in use
        """
        return self._delim


class BaseProcessor(ABC):
    """
        `BaseProcessor` process `Iterable` and return a list of result of `_process_element`.
        - implement `_process_element` in inherited class. 
    """
    def __init__(self, loader: BaseLoader, another_param: object):
        self._loader = loader
        self._another_param = another_param
        
    def process(self, elements: Iterable) -> list[Any]:
        data = self._loader.load()
        result = []
        for element in data:
            output = self._process_element(element)
            result.append(output)
            # print(output)
        return result
    
    @abstractmethod
    def _process_element(self, element: object) -> Any:
        pass

options = {filename: "c:/some/constant/path.txt"}
T = TypeVar(T, bound=BaseLoader)

class SimpleBaseProcessor(BaseProcessor, Generic[T]):
    """
        `SimpleBaseProcessor` process a constant file, and return raw values in situ.
        - indicate `BaseLoader` class. (what data is expected as prerequisite)
        - override `_process_element` in inherited class. 
    """
    def __init__(self, another_param: object):
        loader_class = self.__orig_class__.__args__[0]
        loader = loader_class(options["filename"])
        super().__init__(loader, another_param)
        
    def _process_element(self, element):
        """
            nothing to do. just return raw value. (default)
        """
        return element
    

class LineCountPrefixedProcessor(SimpleBaseProcessor[RawLineLoader]):
    """
        `LineCountPrefixedProcessor` return a list of line count prefixed line strings. 
    """ 
    def __init__(self, another_param, prefix = "{0} | "):
        super().__init__(another_param)
        self._prefix = prefix
        self._count = 0
        
    def _process_element(self, element):
        processed = self._prefix.format(self._count) + element
        self._count += 1
        return processed
    
class ElementQuoteProcessor(SimpleBaseProcessor[ElementTrimmedLineLoader]):
    """
        `ElementQuoteProcessor` process trimmed elements and quote each of them.
    """ 
    def __init__(self, another_param, quote_char = "'"):
        super().__init__(another_param)
        self._quote_char = quote_char
        
    def _quote(self, target):
        q = self._quote
        return q + target + q
        
    def _process_element(self, element):
        delim = self._loader.get_delim() 
        return delim.join(self._quote(elem) for elem in element.split(delim))

Solution

  • When you write something like:

    from typing import Generic, TypeVar
    
    T = TypeVar('T')
    
    class MyGeneric(Generic[T]):
        def __init__(self):
            print(getattr(self, '__orig_class__', None))
    
    x = MyGeneric[int]()
    

    You may see x.orig_class appear on some Python versions and not on others. That is an internal artifact of how the CPython interpreter (and the typing module) tracked Generic information in older versions. It was never specified in any PEP as a stable, user-facing API for “inspect your type parameters at runtime.”

    Since standard Python discards generic parameters at runtime, why not just store them yourself

    from typing import Generic, TypeVar, Type
    
    T = TypeVar('T')
    
    class MyGenericClass(Generic[T]):
        def __init__(self, t: Type[T]):
            self._t = t
    
    x - MyGenericClass(int)
    print(x._t)
    

    Or just pass the value

    class MyGenericClass(Generic[T]):
        def __init(self, examplevalue: T):
            self._t = type(examplevalue)