Search code examples
pythonpycharmmypypython-typing

Reset some object attributes to initial values


Requirement:

I have a class with many fields initialized in __init__ method. Some of these fields should be possible to reset to initial values via a reset() method. I would like to provide typing info for these attributes and make Flake, MyPy, PyCharm (and me) happy with the solution.

Possible solutions:

  1. Duplicate initial values

    In this solution all tools (MyPy, Flake, PyCharm) are happy but not me. I have initial values in two places (__init__ and reset) and I need to keep them in sync. There is a possibility that if in the future one initial value needs to be modified, then I will not change it in both places.

    class Test:
    
      def __init__(self) -> None:
        self.persistentCounter: int = 0
    
        self.resetableCounter: int = 1  # the same value is in reset method. Keep them in sync!!!
        # seven more attributes of different types and initial values
    
      def reset(self) -> None:
        self.resetableCounter = 1  # the same value is in __init__ method. Keep them in sync!!!
        # reset remaining seven attributes to the same values as in __init__()
    
  2. Keep initial values in one method only

    The fix seems to be easy: keep initial values in reset method only and call reset from __init__.

    class Test:
    
      def __init__(self) -> None:
        self.persistentCounter: int = 0
    
        self.reset()
    
      def reset(self) -> None:
        self.resetableCounter: int = 1
        # seven more attributes of different types and initial values
    

    I'm happy (and Flake8 and MyPy too) with such solution but PyCharm complains that

    Instance attribute resetableCounter defined outside __init__

    This warning can be switched off, but sometimes it is useful - when I forgot to define some attribute in neither __init__ nor reset method.

  3. Define attributes as None

    So we can improve the second solution - define attribute inside __init__ but set it as None and then call the reset method.

    from typing import Optional
    
    
    class Test:
    
      def __init__(self) -> None:
        self.persistentCounter: int = 0
    
        self.resetableCounter: Optional[int] = None
        # seven more attributes of different types - all defined as Optional
    
        self.reset()
    
      def reset(self) -> None:
        self.resetableCounter = 1
        # seven more attributes with different initial values
    
      def foo(self) -> bool:
        return self.resetableCounter > 10
    

    The downside of this solution is that the attribute is defined as Optional[int], not int and when I use it for example in the foo method, then mypy complains

    error: Unsupported operand types for < ("int" and "None")
    note: Left operand is of type "Optional[int]"
    

    This can be fixed when I put an additional assert inside foo method:

      def foo(self) -> bool:
        assert self.resetableCounter is not None
        return self.resetableCounter > 10
    

    It makes all tools happy but not me - I do not want to fill the source code with many "unnecessary" assertions.

Question:

How to fulfill the requirement specified above and mitigate downsides of presented solutions?

Instance attribute attribute_name defined outside __init__ describes a similar issue but no answer fits my requirement (see above).


Solution

  • Be inspired by @Wombatz response I ended with this solution:

    class Test:
    
      resetableCounter: int = 1
      # seven more attributes of different types
    
      def __init__(self) -> None:
        self.persistentCounter: int = 0
        self.reset()
    
      def reset(self) -> None:
        if 'resetableCounter' in self.__dict__:
          del self.resetableCounter
        # del also rest seven attributes
    
    

    All tools are happy and the code is clean, brief and readable.

    Many thanks for all contributions.

    Update

    I had to add the if 'resetableCounter' in self.__dict__: line to the code above. Otherwise the reset method failed when it was called from __init__ or when it was called two times in the row without variable modification between first and second call.

    The code is not so nice now (especially the need to write manually the variable name as string) but I think it is still the best (known to me) solution.