Search code examples
pythontkinterwidgetattributeerror

Tkinker - When placing buttons inside a frame = AttributeError: object has no attribute 'tk'


I'm trying to build a more complex GUI for my app. I'm trying to place buttons using .grid() inside of a frame. However, I'm getting "AttributeError: object has no attribute 'tk'" whenever I try to create a button with the frame as the root. I've seen people write GUI classes (i.e. class Frame(tk.Frame)), and that has created more problems with my code. How am I suppose to create buttons and place them within the frame without having to rewrite the majority of my classes from scratch?

When I root a button to the "master," it works fine. However, if it's rooted to the "action_frame" that's when I get the error.

calculator.py

# -*- coding: utf-8 -*-
import tkinter as tk

from math import *
from classes_GUI import ButtonBlock, LabelBlock, TextBlock, FrameBlock
from classes_calculator import ActionBlock


# Store input number
def storeInput(entry_text, result_text, action, array):
    numb = 0.0

    try:
        numb = float(entry_text.retrieveTextInput())
    except:
        print('Please enter a valid number')
        return

    calc_action = ActionBlock(numb, action)
    array.append(calc_action)
    entry_text.clearText()

    num = calc_action.returnNumber()
    act = calc_action.returnAction()

    input_texts = dict([
        (1, ' + ' + str(num)),
        (2, ' - ' + str(num)),
        (3, ' * ' + str(num)),
        (4, ' / ' + str(num)),
        (5, ' + 1/' + str(num)),
        (6, ' + ' + str(num) + '^2')
        ])

    result_text.changeText(input_texts[act])


# Calculate result
def calcResult(entry_text, result_text, array):
    result = 0.0

    for calc in array:
        action = calc.returnAction()
        num = calc.returnNumber()

        if action == 1:
            result += num
        elif action == 2:
            result -= num
        elif action == 3:
            result *= num
        elif action == 4:
            result /= num
        elif action == 5:
            result += 1.0 / num
        elif action == 6:
            result += num ** 2

    entry_text.clearText()
    result_text.changeText(str(result), True)


# Create a new calculator instance
def exeCalc():
    action_blocks = []
    button_blocks = []
    frame_blocks = []
    label_blocks = []
    text_blocks = []

    # Create GUI
    master = tk.Tk()
    master.title('Calculator')

    # Create frames
    action_frame = FrameBlock(master, 30, 30, 1, 6)
    frame_blocks.append(action_frame)

    for f in frame_blocks:
        f.createFrame()

    # Create GUI labels
    title_label = LabelBlock(master, 20, 2, 'n', 0, 0, 'center', 'Calculator')
    label_blocks.append(title_label)
    entry_label = LabelBlock(master, 20, 2, 'n', 1, 0, 'center', 'Enter:')
    label_blocks.append(entry_label)
    result_label = LabelBlock(master, 20, 2, 'n', 2, 0, 'center', 'Result:')
    label_blocks.append(result_label)

    for l in label_blocks:
        l.createLabel()

    # Create GUI text
    entry_text = TextBlock(master, 20, 2, 1, 1, 'normal', '')
    text_blocks.append(entry_text)
    result_text = TextBlock(master, 20, 2, 2, 1, 'disabled', '0')
    text_blocks.append(result_text)

    for t in text_blocks:
        t.createText()

    # Create GUI buttons
    close_button = ButtonBlock(master, 6, 2, 3, 0, 'Close',
        lambda: master.destroy())
    button_blocks.append(close_button)

    add_button = ButtonBlock(frame_blocks[0], 4, 2, 0, 0, '+',
        lambda: storeInput(text_blocks[0], text_blocks[1], 1, action_blocks))
    button_blocks.append(add_button)

    subtract_button = ButtonBlock(frame_blocks[0], 4, 2, 0, 1, '-',
        lambda: storeInput(text_blocks[0], text_blocks[1], 2, action_blocks))
    button_blocks.append(subtract_button)

    multiply_button = ButtonBlock(frame_blocks[0], 4, 2, 0, 2, 'x',
        lambda: storeInput(text_blocks[0], text_blocks[1], 3, action_blocks))
    button_blocks.append(multiply_button)

    divide_button = ButtonBlock(frame_blocks[0], 4, 2, 1, 0, '/',
        lambda: storeInput(text_blocks[0], text_blocks[1], 4, action_blocks))
    button_blocks.append(divide_button)

    fraction_button = ButtonBlock(frame_blocks[0], 4, 2, 1, 1, '1/x',
        lambda: storeInput(text_blocks[0], text_blocks[1], 5,
        action_blocks))
    button_blocks.append(fraction_button)

    square_block = ButtonBlock(frame_blocks[0], 4, 2, 1, 2, 'x^2',
        lambda: storeInput(text_blocks[0], text_blocks[1], 6,
        action_blocks))
    button_blocks.append(square_block)

    equal_button = ButtonBlock(frame_blocks[0], 4, 2, 2, 0, '=',
        lambda: calcResult(text_blocks[0], text_blocks[1], action_blocks))
    button_blocks.append(equal_button)

    for b in button_blocks:
        b.createButton()

    master.mainloop()

classes_GUI.py

# -*- coding: utf-8 -*-
import tkinter as tk


# Create a base data block
class BaseBlock():
    def __init__(self, root, width, height, txt):
        self.root = root
        self.width = width
        self.height = height
        self.txt = txt


# Create a inner data block
class InnerBlock(BaseBlock):
    def __init__(self, root, width, height, row, column, txt):
        super().__init__(root, width, height, txt)
        self.g_row = row
        self.g_column = column


# Create a data block for a button
class ButtonBlock(InnerBlock):
    def __init__(self, root, width, height, row, column, txt, command=None):
        super().__init__(root, width, height, row, column, txt)
        self.command = command

    def createButton(self):
        button = tk.Button(self.root, text=self.txt, width=self.width,
            height=self.height, command=self.command)
        button.grid(row=self.g_row, column=self.g_column)
        return button


# Create a frame data block
class FrameBlock(InnerBlock):
    def __init__(self, root, width, height, row, column, txt=None):
        super().__init__(root, width, height, row, column, txt)

    def createFrame(self):
        frame = tk.Frame(self.root, width=self.width, height=self.height)
        frame.grid(row=self.g_row, column=self.g_column)
        return frame


# Create a data block for a  window
class LabelBlock(InnerBlock):
    def __init__(self, root, width, height, anchor, row, column, justify, txt):
        super().__init__(root, width, height, row, column, txt)
        self.anchor = anchor
        self.justify = justify

    def createLabel(self):
        label = tk.Label(self.root, width=self.width, height=self.height,
            anchor=self.anchor, justify=self.justify, text=self.txt)
        label.grid(row=self.g_row, column=self.g_column)
        return label


# Create a data block for text
class TextBlock(InnerBlock):
    def __init__(self, root, width, height, row, column, state, txt):
        super().__init__(root, width, height, row, column, txt)
        self.state = state
        self.text = None

    def createText(self):
        self.text = tk.Text(self.root, width=self.width, height=self.height)
        self.text.insert(tk.END, self.txt)
        self.text.grid(row=self.g_row, column=self.g_column)
        self.text.config(state=self.state)
        return self.text

    # Clear text
    def clearText(self):
        self.text.delete('1.0', 'end')

    # Change text
    def changeText(self, new_txt, clear=False):
        self.text.config(state='normal')
        if clear:
            self.clearText()
        self.text.insert(tk.END, new_txt)
        self.text.config(state='disabled')

    # Retrieve input from text box
    def retrieveTextInput(self):
        text_input = self.text.get('1.0', 'end')
        return text_input

Solution

  • It helps if your base class actually extends a Frame. ;)

    class BaseBlock(tk.Frame):
        def __init__(self, master, width, height, txt):
            tk.Frame.__init__(self, master)
    

    Really though, you are creating too many layers and managing everything strangely. The below would be better. All this inheriting at every step and creation functions is too obfuscated.

    import tkinter as tk
    from math import *
    from dataclasses import asdict, dataclass
    from typing import Callable  
      
    @dataclass
    class Label_dc:
        width:   int = 20
        height:  int = 2
        anchor:  str = 'n'
        justify: str = 'center'
        text:    str = ''
    
        
    @dataclass
    class Button_dc:
        width:   int      = 4
        height:  int      = 2
        text:    str      = ''
        command: Callable = None    
    
    
    @dataclass
    class Text_dc:
        width:  int = 20
        height: int = 2
        state:  str = 'normal'
    
        
    #from classes_calculator import ActionBlock
    class FrameBlock(tk.Frame):
        def __init__(self, master, row, column, rowspan, **kwargs):
            tk.Frame.__init__(self, master, **kwargs)
            
            self.grid(row=row, column=column, rowspan=rowspan)
    
           
    class ButtonBlock(tk.Button):
        def __init__(self, master, row, column, **kwargs):
            tk.Button.__init__(self, master, **asdict(Button_dc(**kwargs)))
            
            self.grid(row=row, column=column)
    
            
    class LabelBlock(tk.Label):
        def __init__(self, master, row, column, **kwargs):
            tk.Label.__init__(self, master, **asdict(Label_dc(**kwargs)))
            
            self.grid(row=row, column=column)
    
    
    class TextBlock(tk.Text):
        def __init__(self, master, row, column, text='', **kwargs):
            tk.Text.__init__(self, master, **asdict(Text_dc(**kwargs)))
            
            self.grid(row=row, column=column)
            self.insert('1.end', text)
    
        # Clear text
        def clearText(self):
            self.delete('1.0', 'end')
    
        # Change text
        def changeText(self, new_txt, clear=False):
            self.config(state='normal')
            if clear:
                self.clearText()
            self.insert(tk.END, new_txt)
            self.config(state='disabled')
    
        # Retrieve input from text box
        def retrieveTextInput(self):
            return self.get('1.0', 'end')
    
    
    class App(tk.Tk):
        WIDTH  = 800
        HEIGHT = 600
    
        def __init__(self, *args, **kwargs):
            tk.Tk.__init__(self, *args, **kwargs)
        
            # Create GUI labels
            LabelBlock(self, 0, 0, text='Calculator')
            LabelBlock(self, 1, 0, text='Enter:')
            LabelBlock(self, 2, 0, text='Result:')
            
            # Create GUI text
            text_blocks = {
                'entry' : TextBlock(self, 1, 1),
                'result': TextBlock(self, 2, 1, state='disabled', text='0'),
            }
        
            #can't use ButtonBlock for this one ~ self.destroy wont pickle properly
            tk.Button(self, text='Close', width=6, height=2, command=self.destroy).grid(row=3, column=0)
            
            action = []
        
            # Create frames
            frame = FrameBlock(self, 1, 3, 2, width=30, height=30)
            
            # Create GUI buttons
            ButtonBlock(frame, 0, 0, text='+', command=lambda: self.store(*text_blocks, 1, action))
            ButtonBlock(frame, 0, 1, text='-', command=lambda: self.store(*text_blocks, 2, action))
            ButtonBlock(frame, 0, 2, text='*', command=lambda: self.store(*text_blocks, 2, action))
            ButtonBlock(frame, 1, 0, text='/', command=lambda: self.store(*text_blocks, 4, action))
            ButtonBlock(frame, 1, 1, text='1/x', command=lambda: self.store(*text_blocks, 5, action))
            ButtonBlock(frame, 1, 2, text='x^2', command=lambda: self.store(*text_blocks, 6, action))
            ButtonBlock(frame, 2, 0, text='=', command=lambda: self.calc(*text_blocks, action))
    
        def store(self, entry, result, action, array):
            pass #remove this line
            numb = 0.0
        
            try:
                numb = float(entry.retrieveTextInput())
            except:
                print('Please enter a valid number')
                return
        
            calc_action = ActionBlock(numb, action)
            array.append(calc_action)
            entry.clearText()
        
            num = calc_action.returnNumber()
            act = calc_action.returnAction()
        
            input_texts = dict([
                (1, ' + ' + str(num)),
                (2, ' - ' + str(num)),
                (3, ' * ' + str(num)),
                (4, ' / ' + str(num)),
                (5, ' + 1/' + str(num)),
                (6, ' + ' + str(num) + '^2')
                ])
        
            result.changeText(input_texts[act])
            
        # Calculate result
        def calc(self, entry, result, array):
            pass #remove this line
            r = 0.0
        
            for calc in array:
                action = calc.returnAction()
                num = calc.returnNumber()
        
                if action == 1:
                    result += num
                elif action == 2:
                    result -= num
                elif action == 3:
                    result *= num
                elif action == 4:
                    result /= num
                elif action == 5:
                    result += 1.0 / num
                elif action == 6:
                    result += num ** 2
        
            entry.clearText()
            result.changeText(str(r), True)
    
    
    if __name__ == '__main__':
        app = App()
        app.title("Calculator")
        app.geometry(f'{App.WIDTH}x{App.HEIGHT}')
        app.mainloop()
    

    It's worth it to change what you have. Everything will be a lot cleaner and easier to manage. Also, I just did a nice chunk for you, and your way is never going to work. Once you use Frame as the super for your BaseBlock, your Button, Label and Text will all break. Lesson learned: don't tell a bunch of different types of widgets to ultimately extend the same thing.


    If you are absolutely stuck on doing it your way ~ you can do it like this

    class FrameBlock(InnerBlock):
        def __init__(self, root, width, height, row, column, txt=None):
            super().__init__(root, width, height, row, column, txt)
            self.frame = tk.Frame(self.root, width=self.width, height=self.height)
            self.frame.grid(row=self.g_row, column=self.g_column)
    

    and then when you want to use it as the master for the Button use action_frame.frame


    aside

    Your method for calculating results is not going to work, at all. You aren't even considering operator precedence. Use eval(). To show you just how far off you are ... This is what it takes to parse every imaginable math expression that python supports. Even if you stripped it down to just what your calculator supports it would still be bigger than your entire current application.

    class Expression:
        # Clean
        __WHITE: str = '\\s'
        __white: Pattern = re.compile(__WHITE)
    
        __COMM: str = '#\\s.*$'
        __comm: Pattern = re.compile(__COMM)
    
        # Symbolic
        __PARENS: str = '[\\)\\(]'
        __parens: Pattern = re.compile(__PARENS)
    
        __INFIX: str = '[%&+-]|[*/]{1,2}|<<|>>|\\||\\^'
        __infix: Pattern = re.compile(__INFIX)
    
        __TOKEN: str = 'STK([0-9]+)'
        __token: Pattern = re.compile(__TOKEN)
    
        __SYMBOLIC: str = f'{__PARENS}|{__INFIX}'
    
        # Prefix
        __INV: str = '([~]+|~u)?'
    
        # Numeric
        __HEX: str = '([-]?0x[0-9a-f]+)'
        __hex: Pattern = re.compile(__HEX)
        __IHEX: str = f'{__INV}{__HEX}'
        __ihex: Pattern = re.compile(__IHEX)
        __OHEX: str = f'^{__HEX}$'
        __ohex: Pattern = re.compile(__OHEX)
    
        __NUM: str = '([-]?[0-9]+(\\.[0-9]+)?)'
        __num: Pattern = re.compile(__NUM)
        __INUM: str = f'{__INV}{__NUM}'
        __inum: Pattern = re.compile(__INUM)
        __ONUM: str = f'^{__NUM}$'
        __onum: Pattern = re.compile(__ONUM)
    
        __NUMERIC: str = f'{__IHEX}|{__INUM}'
    
        # Variable
        __HYPER: str = 'acosh|asinh|atanh|cosh|sinh|tanh'
        __TRIG: str = 'acos|asin|atan2|atan|cos|sin|tan|hypot|dist'
        __THEORY: str = 'ceil|comb|fabs|factorial|floor|fmod|frexp|gcd|isqrt|ldexp|modf|perm|remainder|trunc'
        __LOG: str = 'expm1|exp|log1p|log10|log2|log|pow|sqrt'
        __ANGLE: str = 'degrees|radians'
        __SPEC: str = 'erfc|erf|lgamma|gamma'
        __FN: str = f'{__HYPER}|{__TRIG}|{__THEORY}|{__LOG}|{__ANGLE}|{__SPEC}'
        __func: Pattern = re.compile(__FN)
    
        __RAND: str = '(random|rand)'
        __rand: Pattern = re.compile(__RAND)
    
        __CONST: str = 'pi|e|tau|inf|' + __RAND
        __const: Pattern = re.compile(__CONST)
    
        __BITWISE: str = '<<|>>|\\||\\^|&'
        __bitwise: Pattern = re.compile(__BITWISE)
    
        __FN2: str = 'min|max|' + __RAND
        __func2: Pattern = re.compile(__FN2)
    
        __VARIABLE: str = f'{__FN}|{__FN2}|{__CONST}'
    
        __SIMPLE: str = f'^({__INUM}+{__INFIX})+{__INUM}$'
        __simple: Pattern = re.compile(__SIMPLE)
    
        # Combo
        __MATH: str = f'{__VARIABLE}|{__NUMERIC}|{__SYMBOLIC}|,|E|\\s'
        __math: Pattern = re.compile(__MATH)
    
        # Priorities
        __P1: str = '[*/]{1,2}|%'
        __P2: str = '[+-]'
        __P3: str = '<<|>>|&'
        __P4: str = '\\||\\^'
        __priority: List[Pattern] = [re.compile(__P1), re.compile(__P2), re.compile(__P3), re.compile(__P4)]
    
        def __init__(self):
            self.value = math.nan
    
        def evaluate(self, expr: str) -> float:
            self.value = Expression.eval(expr)
            return self.value
    
        @staticmethod
        def __hexrepl(m: Match[Union[str, bytes]]):
            return str(int(m.group(0), 16))
    
        @staticmethod
        def eval(expr: str, fast: bool = False) -> float:
            # Remove Whitespace, Comments, Convert Hash To Hex and Case To Lower
            expr = Expression.__comm.sub("", expr)
            expr = Expression.__white.sub("", expr)
            expr = expr.replace('#', '0x').lower()
    
            # Check If This Is Actual Math By Deleting Everything Math Related And Seeing If Anything Is Left
            if len(re.sub(Expression.__math, "", expr)) > 0:
                return math.nan
    
            if fast:
                return Expression.__fast(expr)
    
            # Parse All Inversions Now ... invert(~) is the only "left side only" operator
            expr = Expression.__parse_inversions(expr)
            expr = Expression.__hex.sub(Expression.__hexrepl, expr)
    
            # Check If This Is Solely A Number ~ If So, Parse Int And Return
            if Expression.__onum.match(expr):
                n = float(expr)
                return int(n) if n % 1 == 0 else n
    
            # We Got This Far. It Must Be Math
            n = Expression.__parse(expr)
            return int(n) if n % 1 == 0 else n
    
        # Private Static Interfaces
    
        @staticmethod
        def __parse_inversions(expr: str) -> str:
            match: Iterator[Match[Union[str, bytes]]] = Expression.__ihex.finditer(expr)
            m: Match[Union[str, bytes]]
            for m in match:
                expr = Expression.__invert_expr(expr, m, 16)
            match = Expression.__inum.finditer(expr)
            for m in match:
                expr = Expression.__invert_expr(expr, m, 10)
            return expr
    
        @staticmethod
        def __invert_expr(expr: str, m: Match[Union[str, bytes]], b: int) -> str:
            t1: str = m.group(1)
            t2: str = m.group(2)
            if t1:
                if t1 == '~u':
                    n: int = Expression.__uinvert_num(int(t2, b))
                else:
                    f: int = len(t1) % 2 == 1
                    n: int = -(int(t2, b) + 1) if f else int(t2, b)
                expr = expr.replace(m.group(0), str(n))
            return expr
    
        @staticmethod
        def __uinvert_num(num: float) -> int:
            if num > 0:
                x: int = int(math.log(num, 2.0) + 1)
                i: int = 0
                for i in range(0, x):
                    num = (num ^ (1 << i))
            return num
    
        @staticmethod
        def __parse(expr: str) -> float:
            exp_stack: List[str] = []
            ops_stack: List[str] = []
            res_stack: List[float] = []
    
            tokens = Expression.__tokenize(expr)
    
            # everything that can come before an operator
            b1: str = f'{Expression.__HEX}|{Expression.__NUM}|{Expression.__CONST}|\\)'
            c: Pattern = re.compile(b1)
    
            # before an operator that is the rest of this expression
            b2: str = f'{Expression.__NUM}E'
            d: Pattern = re.compile(b2, re.I)
    
            expr = tokens.expression[0::]
    
            while len(expr):
                m: Match[Union[str, bytes]] = Expression.__infix.search(expr)
                if m:
                    op: str = m.group()
                    left: str = expr[0:m.span()[0]]
                    if re.search(c, left) and not re.search(d, left):
                        exp_stack.append(left)
                        ops_stack.append(op)
                        expr = expr.replace(f'{left}{op}', "")
                    else:
                        if len(left) == 0 or re.match(d, left):
                            right: str = expr[m.span()[1]::]
                            m = Expression.__infix.search(right)
                            if m:
                                left = f'{left}{op}'
                                op = m.group()
                                left = f'{left}{right[0:m.span()[0]]}'
                                exp_stack.append(left)
                                ops_stack.append(op)
                                expr = expr.replace(f'{left}{op}', "")
                            else:
                                exp_stack.append(expr)
                                expr = ""
                        else:
                            # Probably Not Even Possible In A Valid Math Expression
                            print("Expression.parse(expr:String): unexpected left side")
                            print("expression: ", expr)
                            print("left side: ", left)
                            print("operator: ", op)
                            print("exp_stack: ", exp_stack)
                            print("ops_stack: ", ops_stack)
                else:
                    exp_stack.append(expr)
                    expr = ""
    
            for r in range(len(exp_stack)):
                m: Match[Union[str, bytes]] = Expression.__token.search(exp_stack[r])
                inner: str = ""
                if m:
                    i: int = int(m.group(1))
                    inner = tokens.stack[i]
    
                res_stack.append(Expression.__parsetype(exp_stack[r], inner))
    
            # Iterate Through Stacks Based On Priority and Do Assignments ~ ie... Calculate Everything
            if len(ops_stack) > 0:
                p: int = 0
                for p in range(len(Expression.__priority)):
                    n: int = 0
                    while n < len(ops_stack) and len(ops_stack) > 0:
                        m: Match[Union[str, bytes]] = Expression.__priority[p].match(ops_stack[n])
                        if m is not None:
                            if not math.isnan(res_stack[n]) and not math.isnan(res_stack[n + 1]):
                                res_stack[n] = Expression.__value(res_stack[n], ops_stack[n], res_stack[n + 1])
                                res_stack.pop(n + 1)
                                ops_stack.pop(n)
                        else:
                            n += 1
    
            return res_stack[0] if len(res_stack) == 1 else math.nan
    
        @staticmethod
        def __parsetype(expr: str, val: str = "") -> float:
            fin: float = math.nan
    
            if val != "":
                tokens: Tokens_t = Expression.__tokenize(val)
                csv: List[str] = tokens.expression.split(",")
                a: float = 0
                b: float = 0
                f: str = ""
                ln: int = len(csv)
    
                if ln >= 1:
                    a = Expression.__parse(Expression.__detokenize(csv[0], tokens))
                if ln == 2:
                    b = Expression.__parse(Expression.__detokenize(csv[1], tokens))
    
                m: Match[Union[str, bytes]] = Expression.__func.match(expr)
                m2: Match[Union[str, bytes]] = Expression.__func2.match(expr)
                if m:
                    f = m.group()
                    fin = getattr(math, f)(a, b) if len(csv) == 2 else getattr(math, f)(a)
                elif m2:
                    f = m2.group()
                    if ln == 2:
                        if f == 'min':
                            fin = min(a, b)
                        elif f == 'max':
                            fin = max(a, b)
                    elif ln == 1:
                        if Expression.__rand.match(f):
                            fin = random() * a
                else:
                    fin = Expression.__parse(val)
            else:
                m: Match[Union[str, bytes]] = Expression.__const.match(expr)
                c: Match[Union[str, bytes]] = Expression.__hex.match(expr)
                if m:
                    cn: str = m.group()
                    fin = random() if Expression.__rand.match(cn) else getattr(math, cn)
                elif c:
                    fin = int(c.group(), 16)
                else:
                    fin = float(expr)
    
            return fin
    
        @staticmethod
        def __tokenize(expr: str) -> Tokens_t:
            c: int = 0
            b: int = -1
            e: int = -1
            ex: str = expr[0::]
            s: List[str] = []
            m: Match[Union[str, bytes]]
            p: Iterator[Match[Union[str, bytes]]] = Expression.__parens.finditer(ex)
            for m in p:
                if m.group() == "(":
                    c += 1
                    if b == -1:
                        b = m.span()[1]
                elif m.group() == ")":
                    c -= 1
                    if c == 0 and b > -1:
                        e = m.span()[0]
                        if b != e:
                            s.append(expr[b:e])
                            ex = ex.replace(expr[b:e], f'STK{len(s) - 1}')
                        b = -1
    
            return Tokens_t(ex, s)  # Tokens_t ~ python equivalent to my c++ math parser
    
        @staticmethod
        def __detokenize(part: str, tokens: Tokens_t) -> str:
            ex: str = part[0::]
            m: Match[Union[str, bytes]]
            p: Iterator[Match[Union[str, bytes]]] = Expression.__token.finditer(ex)
            for m in p:
                ex = ex.replace(m.group(0), tokens.stack[int(m.group(1))])
            return ex
    
        @staticmethod
        def __fast(expr: str) -> float:
            return eval(expr)
    
        __ops: Dict[str, Callable] = {
            '+': lambda x, y: x + y,
            '-': lambda x, y: x - y,
            '*': lambda x, y: x * y,
            '/': lambda x, y: x / y,
            '**': lambda x, y: x ** y,
            '//': lambda x, y: x // y,
            '>>': lambda x, y: x >> y,
            '<<': lambda x, y: x << y,
            '&': lambda x, y: x & y,
            '|': lambda x, y: x | y,
            '^': lambda x, y: x ^ y,
            '%': lambda x, y: x % y,
        }
    
        @staticmethod
        def __value(v1: float, oper: str, v2: float) -> float:
            x: float = 0
            try:
                m: Match[Union[str, bytes]] = Expression.__bitwise.match(oper)
                x = Expression.__ops[oper](v1, v2) if not m else Expression.__ops[oper](int(v1), int(v2))
            except KeyError:
                x = math.nan
            return x