Structure which avoids circular import in Python

34 Views Asked by At

I'm building float point model in python. Floating point formats are defined in class, and its operations such as multiply ... etc are defined in other modules as class. Below is directory structure:

.
├── README.md
├── chess.py
├── float_class
│   ├── __init__.py
│   ├── bitstring.py
│   ├── floatbase.py
│   ├── floatclass.py
│   ├── hw_model
│   │   ├── __init__.py
│   │   ├── fp_add
│   │   │   ├── __init__.py
│   │   │   └── fpmul.py
...
└── test.py

float_class.floatclass has Bfloat16/Float32 class which describes definition and behavior of fp itself. float_class.hw_model.fp_add.fpadd has FloatAddition class which describes behavioral model of float point addition described with classes of float_class.bitstring, which has bit manipulation class, BitString. And Bfloat16/Float32 gets these hw_model.{operations} as instances to make it as its own operation method. Here's some snippet of my codes:

floatclass.py

from abc import ABCMeta, abstractmethod, abstractclassmethod
from typing import Tuple, Optional, ClassVar, Union, TypeVar, Type, Generic, List
from typing_extensions import Self, TypeAlias

from float_class.hw_model.fp_mul.fpmul import FloatMultiplication as Mul


class FloatBase(metaclass=ABCMeta):
    """
    Base class for floating-point numbers.
    """
    _sign_bitpos: int = 64
    _exponent_bits: int = 11
    _mantissa_bits: int = 52

    @property
    def sign_bitpos(self) -> int:
        return self._sign_bitpos

    @property
    def exponent_bits(self) -> int:
        return self._exponent_bits

    @property
    def mantissa_bits(self) -> int:
        return self._mantissa_bits

    @property
    def bias(self) -> int:
        return (1 << (self._exponent_bits - 1)) - 1

    @property
    def exp_max(self) -> int:
        return (1 << (self._exponent_bits - 1))

    @property
    def mant_max(self) -> int:
        return (1 << self._mantissa_bits) - 1

    @classmethod
    def _bias(cls) -> int:
        return (1 << (cls._exponent_bits - 1)) - 1

    @classmethod
    def _exp_max(cls) -> int:
        return (1 << (cls._exponent_bits - 1))

    @classmethod
    def _mant_max(cls) -> int:
        return (1 << cls._mantissa_bits) - 1

    def __init__(self, sign: int, exponent: int, mantissa: int,     
                _sign_bitpos: int = 64, 
                _exponent_bits: int = 11, 
                _mantissa_bits: int = 52) -> None:
        # call setter in __init__
        self._sign_bitpos = _sign_bitpos
        self._exponent_bits = _exponent_bits
        self._mantissa_bits = _mantissa_bits
        self.set(sign, exponent, mantissa)

    def set(self, sign: int, exponent: int, mantissa: int) -> None:
        self.sign: int = self.set_sign(sign)
        self.exponent: int = self.set_exponent(exponent)
        self.mantissa: int = self.set_mantissa(mantissa)

    def set_sign(self, sign: int) -> int:
        if not (sign == 0 or sign == 1):
            raise FloatValueError(f"{self.__class__.__name__} sign value must be 0 or 1")
        return sign

    def set_exponent(self, exponent: int) -> int:
        if not 0 - self.bias <= exponent <= self.exp_max:
            raise FloatValueError(f"{self.__class__.__name__} exponent value must be in range of {-self._bias()} ~ {self.exp_max}")
        return exponent

    def set_mantissa(self, mantissa: int) -> int:
        if not 0 <= mantissa <= self.mant_max:
            raise FloatValueError(f"{self.__class__.__name__} mantissa value must be in range of 0 ~ {self.mant_max}")
        return mantissa
      
    # Operations in HW component
    def __mul__(self, other: Self) -> Self:
        if not isinstance(other, FloatBase):
            raise FloatTypeError("Both operands should be FloatBase objects.")
        if type(self) is not type(other):
            raise FloatTypeError("Both operands should be of the same type.")
        multiplication = Mul(self, other)
        return multiplication.multiply()
...


class Bfloat16(FloatBase):
    _sign_bitpos = 16
    _exponent_bits = 8
    _mantissa_bits = 7

    def __init__(self, sign: int, exponent: int, mantissa: int) -> None:
        super().__init__(sign, exponent, mantissa, self._sign_bitpos, self._exponent_bits, self._mantissa_bits)
...

class Float32(FloatBase):
    _sign_bitpos = 32
    _exponent_bits = 8
    _mantissa_bits = 23

    def __init__(self, sign: int, exponent: int, mantissa: int) -> None:
        super().__init__(sign, exponent, mantissa, self._sign_bitpos, self._exponent_bits, self._mantissa_bits)

fp_mul.py

from float_class import *
from typing import Generic, TypeVar

FloatBaseT = TypeVar('FloatBaseT', bound='FloatBase')


class FloatMultiplication(Generic[FloatBaseT]):
    def __init__(self, a: FloatBaseT, b: FloatBaseT) -> None:
        self.a = a
        self.b = b

    def multiply(self):
        # If input is Bfloat16, bf16_to_fp32
        # Make flag of bf16 input
        bf16_input = isinstance(self.a, bf16) & isinstance(self.b, bf16)
...
        return

float_class._init_.py

from .floatclass import Bfloat16 as bf16
from .floatclass import Float32 as fp32
from .floatclass import FloatBase

__all__ = ['bf16', 'fp32', 'FloatBase']

test.py

a = bf16(0, 0, 0)
b = bf16(0, 1, 0)
c = a * b

Which returns: ''' Traceback (most recent call last): File "/root/workspace/git/Bfloat16/test.py", line 107, in c = a * b File "/root/workspace/git/Bfloat16/float_class/floatclass.py", line 184, in mul return multiplication.multiply() File "/root/workspace/git/Bfloat16/float_class/hw_model/fp_mul/fpmul.py", line 17, in multiply bf16_input = isinstance(self.a, bf16) & isinstance(self.b, bf16) NameError: name 'bf16' is not defined '''

I don't get it because bf16 is already defined in fpmul.py! Also, I have trouble in compilcate circular import issue with this. How can I avoid this issue? If I need to refactor or modulize my code, how can it be done? It will be very much grateful with your help.

0

There are 0 best solutions below