Help with Calling Function from Dictionary

Good afternoon, all. I need some help with my program. I anticipate the program growing steadily larger as I add more subjects to the program, so I’m trying to avoid the use of if/else statements. To do so, I’ve tried using a dictionary that will call the function through a while loop. When doing so, I obtain an attribute issue ‘AttributeError: ‘QuestionWindow’ object has no attribute ‘QuestionWindow’’. I’ve tried different indentations, moving the function within the program, and changing some of the names, but I’m unable to get it to work. I’m still very new to Python and self learning, so I’m also posting the entire code to get feedback on how I might be able to make it more readable or possibly reduce the line count as well as possibly being given an explanation on how certain aspects why certain aspects should be changed. Any help will be greatly appreciated.


import tkinter as tk
from tkinter import *
from PIL import ImageTk,Image
import random


class QuestionWindow():
    
    #This will open up the question window and allow the user to study
    def aminoSelect(self):

        AminoAcidList = [
            
        ["Alanine", "Ala", "A", 2.34, 9.69],
        ["Arginine", "Arg", "R", 2.17, 9.04, 12.48],
        ["Asparagine", "Asn", "N", 2.02, 8.84],
        ["Aspartic Acid", "Asp", "D", 2.09, 9.82, 3.71],
        ["Cysteine", "Cys", "C", 1.71, 10.78, 8.33],
        ["Glutamine", "Gln", "Q", 2.17, 9.13],
        ["Glutamic Acid", "Glu", "E", 2.19, 9.67, 4.15],
        ["Glycine", "Gly", "G", 2.34, 9.60],
        ["Histidine", "His", "H", 1.82, 9.17, 6.00],
        ["Isoleucine", "Ile", "I", 2.36, 9.68],
        ["Leucine", "Leu", "L", 2.36, 9.60],
        ["Lysine", "Lys", "K", 2.18, 8.95, 10.53],
        ["Methionine", "Met", "M", 2.28, 9.21],
        ["Phenylalanine", "Phe", "F", 2.20, 9.13],
        ["Proline", "Pro", "P", 1.99, 10.96],
        ["Serine", "Ser", "S", 2.21, 9.15],
        ["Threonine", "Thr", "T", 2.11, 9.62],
        ["Tryptophan", "Trp", "W", 2.38, 9.39],
        ["Tyrosine", "Tyr", "Y", 2.20, 9.11, 10.07],
        ["Valine", "Val", "V", 2.32, 9.62]
        ]

        AminoAcid = random.choice(AminoAcidList)

        return AminoAcid


    #######################################################

    def ChargeDetermination(self, pHVal, aminoChain):

        pH = pHVal
        Amino = []
        Amino.append(aminoChain)
        PosPka3 = ['R', 'K', 'H']
        NegPka3 = ['D', 'E', 'C', 'Y']
        self.TotalCharge = 0
        X = 0

        while X < len(Amino):

            if Amino[X][2] in PosPka3:

                if pH < Amino[X][5]:
                    self.TotalCharge += 1

                elif pH == Amino[X][5]:
                    self.TotalCharge += .5

            if Amino[X][2] in NegPka3:

                if pH > Amino[X][5]:
                    self.TotalCharge -= 1

                elif pH == Amino[X][5]:
                    self.TotalCharge -= .5

            if pH < Amino[0][4]:
                self.TotalCharge += 1

            elif pH == Amino[0][4]:
                self.TotalCharge += .5

            if pH > Amino[-1][3]:
                self.TotalCharge -= 1

            elif pH == Amino[-1][3]:
                self.TotalCharge -= .5

            X = X+1
     
        return self.TotalCharge


    #######################################################

    #This will ask for ID if ID questions are selected

    def AminoAcidID(self):       

        self.Quest1Lbl = Label(self.QuestionWindow, text = "What is the full name of the amino acid?", wraplength = 300)
        self.Quest1Lbl.pack()
        self.Quest1Answer = Entry(self.QuestionWindow, width = 20, bg = "yellow")
        self.Quest1Answer.pack()
        self.Quest1Reveal = Label (self.QuestionWindow, text = " ", width = 20)
        self.Quest1Reveal.pack()

        self.Quest2Lbl = Label(self.QuestionWindow, text = "What is the three letter code of the amino acid?", wraplength = 300).pack()
        self.Quest2Answer = Entry(self.QuestionWindow, width = 20, bg = "yellow")
        self.Quest2Answer.pack()
        self.Quest2Reveal = Label (self.QuestionWindow, text = " ", width = 20)
        self.Quest2Reveal.pack()

        self.Quest3Lbl = Label(self.QuestionWindow, text = "What is the one letter code of the amino acid?", wraplength = 300).pack()
        self.Quest3Answer = Entry(self.QuestionWindow, width = 20, bg = "yellow")
        self.Quest3Answer.pack()
        self.Quest3Reveal = Label(self.QuestionWindow, text = " ", width = 20)
        self.Quest3Reveal.pack()

 
    #######################################################

    def pKaQuestion(self):

        self.pHVal = round(random.uniform(0, 14), 2)

        self.aminoChain = self.AcidName

        self.TotalCharge = self.ChargeDetermination(self.pHVal, self.aminoChain)

        self.Quest4Lbl = Label(self.QuestionWindow, text = f"What is the amino acids charge at pH {self.pHVal: .2f}?")
        self.Quest4Lbl.pack()

        self.Quest4Answer = Entry(self.QuestionWindow, width = 20, bg = "yellow")
        self.Quest4Answer.pack()

        self.Quest4Reveal = Label (self.QuestionWindow, text = ' ')
        self.Quest4Reveal.pack()

    #######################################################

    #This will show the questions of all question types

    def CombinedWindow(self):

        self.AminoAcidID()
        self.pKaQuestion()

    #######################################################

    #This will validate the entered answer and show if they are correct

    def CheckAnswer(self):

        if self.IDCall == 1:

            if len(self.Quest1Answer.get()) == 0 or len(self.Quest2Answer.get()) == 0 or len(self.Quest3Answer.get()) == 0:
               self.ErrorMsgLbl.config(text = "Please Enter A Value Into All Fields", fg = "red")                                                           

            else:
                self.ShowAnswerBtn.config(state = ACTIVE)
                self.NameInput = self.Quest1Answer.get()
                self.Let3Input = self.Quest2Answer.get()
                self.Let1Input = self.Quest3Answer.get()

                if self.AcidName[0].upper() == self.NameInput.upper():
                    self.Quest1Answer.config(bg = "green")

                else:
                    self.Quest1Answer.config(bg = "red")

                if self.AcidName[1].upper() == self.Let3Input.upper():
                    self.Quest2Answer.config(bg = "green")

                else:
                    self.Quest2Answer.config(bg = "red")

                if self.AcidName[2].upper() == self.Let1Input.upper():
                    self.Quest3Answer.config(bg = "green")

                else:
                    self.Quest3Answer.config(bg = "red")

        if self.pKCall == 1:
            
            if len(self.Quest4Answer.get()) == 0:
                self.ErrorMsgLbl.config(text = "Please Enter A Value Into All Fields", fg = "red")       

            else:
                self.ShowAnswerBtn.config(state = ACTIVE)
                self.pKAnswerInt = int(self.Quest4Answer.get())
                self.ErrorMsgLbl.config(text = '')

                if self.pKAnswerInt == self.TotalCharge:
                    self.Quest4Answer.config(bg = "green")

                else:
                    self.Quest4Answer.config(bg = "red")

    #######################################################

     #This defines which amino acid is used

    def AminoChoice(self):

        self.NewAcidName = self.aminoSelect()
        self.NewAminoAcidPic = Image.open(f"{self.NewAcidName[0]}.png")
        self.NewAminoAcidPicResize = self.NewAminoAcidPic.resize((200, 200))
        self.NewActualImage = ImageTk.PhotoImage(self.NewAminoAcidPicResize)
        self.QuestionBox.configure(image = self.NewActualImage)
        self.QuestionBox.photo = self.NewActualImage
        self.QuestionBox.config(text = self.NewAcidName[0])
        self.AcidName = self.NewAcidName
        self.ShowAnswerBtn.config(state = DISABLED)
        self.ErrorMsgLbl.config(text = ' ')

        if self.IDCall == 1:
            self.Quest1Answer.config(bg = "yellow")
            self.Quest1Answer.delete(0,END)
            self.Quest1Reveal.config(text = ' ')

            self.Quest2Answer.config(bg = "yellow")
            self.Quest2Answer.delete(0,END)
            self.Quest2Reveal.config(text = ' ')

            self.Quest3Answer.config(bg = "yellow")
            self.Quest3Answer.delete(0,END)
            self.Quest3Reveal.config(text = ' ')

        if self.pKCall == 1:
            self.Quest4Answer.config(bg = "yellow")
            self.Quest4Answer.delete(0, END)
            self.NewpHVal = round(random.uniform(0, 14), 2)

            self.TotalCharge = self.ChargeDetermination(self.NewpHVal, self.AcidName)

            self.Quest4Lbl.config(text = f"What is the amino acids charge at pH {self.NewpHVal: .2f}?")
            self.Quest4Reveal.config(text = ' ')

    #######################################################

    #This will show the answer when pressed
    def RevealAnswer(self):

        if self.IDCall == 1:
            self.Quest1Reveal.config(text = self.AcidName[0])
            self.Quest2Reveal.config(text = self.AcidName[1])
            self.Quest3Reveal.config(text = self.AcidName[2])

        if self.pKCall == 1:
            self.TotalChargeString = str(self.TotalCharge)
            self.Quest4Reveal.config(text = self.TotalChargeString)               

    #######################################################

    def openQuestionWindow(self):

        self.QuestionWindow = Toplevel(root)
        self.QuestionWindow.title('Question Window')
        self.IDCall = 0
        self.pKCall = 0

        self.AcidName = self.aminoSelect()
        self.AminoAcidPic = Image.open(f"{self.AcidName[0]}.png")
        self.AminoAcidPicResize = self.AminoAcidPic.resize((200, 200))
        self.ActualImage = ImageTk.PhotoImage(self.AminoAcidPicResize)
        self.QuestionBox = Label(self.QuestionWindow, image = self.ActualImage)
        self.QuestionBox.pack()
        self.QuestionBox.config(image = self.ActualImage)

        self.ErrorMsgLbl = Label(self.QuestionWindow, text = ' ')
        self.ErrorMsgLbl.pack()

        PossibleFuncList = ['AminoID', 'GenpKa', 'ActpKa']

        CallList = AddtoFunList()

        for Call in PossibleFuncList:

            for PossibleFuncList in CallList:

                CallList[PossibleFuncList]()

            

        if (SelName_V.get() == 1) & ((SelGenPk_V.get() == 1) or (SelActPk_V.get() == 1)):
            self.IDCall += 1
            self.pKCall += 1
            self.CombinedWindow()

##        elif (SelName_V.get() == 1) & ((SelGenPk_V.get() == 0) or (SelActPk_V.get() == 0)):
##            self.IDCall +=1
##            self.AminoAcidID()

        elif (SelName_V.get() == 0) & ((SelGenPk_V.get() == 1) or (SelActPk_V.get() == 1)):
            self.pKCall += 1
            self.pKaQuestion()

        self.CheckAnswerBtn = Button(self.QuestionWindow, text = "Check Answers", command = self.CheckAnswer).pack(padx = 5, pady = 5,side = RIGHT)
        self.NewAminoBtn = Button(self.QuestionWindow, text = "Change Amino", command = self.AminoChoice).pack(pady = 5, padx = 5,  side = RIGHT)
        self.CloseBtn = Button(self.QuestionWindow, text = "Close", command = lambda: self.QuestionWindow.destroy()).pack(padx = 5, pady = 5, side = LEFT)
        self.ShowAnswerBtn = Button (self.QuestionWindow, text = "Show Answers", command = self.RevealAnswer)
        self.ShowAnswerBtn.pack(pady = 5, padx = 5, side = LEFT)
        self.ShowAnswerBtn.config(state = DISABLED)
   
#This will act as the main, interactable window

def AddtoFunList():
    FunList = {}

    AminoID = {'AminoID' : QuestionWindow().AminoAcidID}
    GenpKa = {'GenpKa' : QuestionWindow().pKaQuestion}

    
    if SelName_V.get() == 1:
        FunList.update(AminoID)

    elif SelName_V.get() == 0 and 'AminoID' in FunList:
        FunList.pop(AminoID)

    if SelGenPk_V.get() == 1:
        FunList.append(GenPk)

    elif SelGenPk_V.get() == 0 and 'GenPk' in FunList:
        FunList.remove('GenPk')

    if SelActPk_V.get() == 1:
        FunList.append('ActPk')

    elif SelActPk_V.get() == 0 and 'ActPk' in FunList:
        FunList.remove('ActPk')

    return FunList

 
def SwitchState():

    UpdatedFunList = AddtoFunList()
    print(UpdatedFunList)

    if UpdatedFunList == {}:
        QuestionOpenbtn.config(state = DISABLED)

    else:
        QuestionOpenbtn.config(state = NORMAL)

    if SelGenPk_V.get() == 1:
        SelActPk.config(state = DISABLED)

    elif SelActPk_V.get() == 1:
        SelGenPk.config(state = DISABLED)

    else:
        SelActPk.config(state = NORMAL)
        SelGenPk.config(state = NORMAL)


root = tk.Tk()
root.title("Biochemistry Start Window")

Introlbl = Label(root, text = "Welcome to the amino acid study aid. Down below you can choose what you'd like to focus on.", wraplength = 300, font =  16).pack(padx = 5, pady = 5)

CheckFrame = tk.Frame(root, relief = 'ridge', highlightbackground = 'black', highlightthickness = 2)
CheckFrame.pack(padx = 20, pady = 20)

SelName_V = IntVar()
SelGenPk_V = IntVar()
SelActPk_V = IntVar()

SelName = Checkbutton(CheckFrame, text = "Amino Acid Identification", command = SwitchState, variable = SelName_V, onvalue = 1, offvalue = 0)
SelName.pack(padx = 5, pady = 5)

SelGenPk = Checkbutton(CheckFrame, text = "Amino Charge, Rounded Values", command = SwitchState, variable = SelGenPk_V, onvalue = 1, offvalue = 0)
SelGenPk.pack(padx = 5, pady = 5)

SelActPk = Checkbutton(CheckFrame, text = "Amino Charge, Actual Values", command = SwitchState, variable = SelActPk_V, onvalue = 1, offvalue = 0)
SelActPk.pack(padx = 5, pady = 5)

QuestionOpenbtn = Button(root, text = "Show Selection Questions", command = QuestionWindow().openQuestionWindow)
QuestionOpenbtn.pack(padx = 5, pady = 5)

CloseWindowBtn = Button(root, text = "Close", command = lambda: root.destroy()).pack(padx = 5, pady =5)

QuestionOpenbtn.config(state = DISABLED)

NoteLbl = Label(root, text = "Note: Rounded values will have the C-Termini and N-Termini pKas rounded to 2.2 and 9.8 respectively.\n", wraplength = 300).pack(padx = 5, pady = 5)

root.mainloop()

You’ve named the class QuestionWindow and then you’re trying to pass self.QuestionWindow to the Tk widgets, but there are 2 problems:

  1. Neither the class nor its instances have an attribute called QuestionWindow (that’s the name of the class itself).

  2. The class QuestionWindow isn’t a Tk widget, so it can’t be a parent of a Tk widget.

The way I’d approach it is to make the class a subclass of the Tk class, like this:

import tkinter as tk

class App(tk.Tk):
    def __init__(self):
        tk.Tk.__init__(self)
        self.title("Biochemistry Start Window")

App().mainloop()

I have import tkinter as tk but not from tkinter import * because importing everything from a module is more trouble than it’s worth - you never know how many names it’s going to import.

Another point:

Introlbl = Label(root, text = "Welcome to the amino acid study aid. Down below you can choose what you'd like to focus on.", wraplength = 300, font =  16).pack(padx = 5, pady = 5)

This will make the label and pack it on the root, but pack returns None, so Introlbl will be None.

Using the example above, I’d put it into __init__ like this:

self.Introlbl = tk.Label(self, text = "Welcome to the amino acid study aid. Down below you can choose what you'd like to focus on.", wraplength = 300, font =  16)
self.Introlbl.pack(padx = 5, pady = 5)

However, as you never refer to Introlbl in the rest of the code, there’s no point in keeping a reference to it anyway, so in this instance:

tk.Label(self, text = "Welcome to the amino acid study aid. Down below you can choose what you'd like to focus on.", wraplength = 300, font =  16).pack(padx = 5, pady = 5)
2 Likes

The code is a little long for a question, but here are my comments:

  • Unless you are never going to share code, keep lines down to 80 characters. These boxes allow 87.
    123456789012345678901234567890123456789012345678901234567890123456789012345678901234567
    Other web pages and mail readers may be narrower. Code is hardly readable when wrapped or hidden.

  • Fewer blank lines: if/elif/else blocks should have no blanks since they usually represent one thought. The blank lines after for loop headers are not needed.

  • No space around ‘=’ in calls (see PEP 8): config(bg='green').

  • Use conditional expressions when appropriate. The pattern

if condition:
    something(a)
else:
    something(b)

can be reduced to one of two patterns, according to the length of condition.

something(a if condition else b)  # Condition is short.

ab = a if condition else b  # Condition is long.
something(ab)

Example from your code with a long condition. This version is easier to understand because it is immediately clear that the same thing is being done, either to ‘red’ or ‘green’, regardless of the condition.

bg = 'green' if self.AcidName[0].upper() == self.NameInput.upper() else 'red'
self.Quest1Answer.config(bg=bg)
  • I learned to separate widget creation from widget placement. If a permanemt self. prefix is not needed, skip it (it can be added later), but a name makes finding the appropriate line much easier when you want to edit a widget call. Grouping .pack or .grid calls makes it easier to rearrange widgets. I recommend learning to use .grid.

  • You might learn more about writing decent python and tkinter code from /Lib/idlelib modules and from perusing stackoverflow.com tag [tkinter] questions with answers.

1 Like

I really appreciate the help and have made the adjustments, but I’m having an issue with the dictionary not being returned even when referenced. I’m not sure if I should edit my code to put the updated information or what would be the best way to handle it. I’m in completely new territory here. What I’m anticipating is that once everything is said and done that I can use the ‘PossibleFuncList’ in conjunction with the dictionary to make calls to particular functions, but for some reason the dictionary becomes emptied when I try to iterate through it. Please let me know if I should post the updated code or if I should just post here what’s been changed.

Thank you for the feedback. I’m still very new to a lot of this, as I’ve only really taken one class on C++ and one on VB. I’m self teaching Python, so the finer things tend to slip by me. I’ve made the adjustments and the code looks much neater now.

In openQuestionWindow, you assign a list to PossibleFuncList and iterate over that list in the outer for loop, but the inner for loop assigns back to PossibleFuncList, which will work, but is confusing.

In AddtoFunList, you have FunList = {}. Despite its name, FunList is a dict because {} is an empty dict.

Then you have FunList.pop(AminoID). The argument of must be a key, but AminoID is a dict.

And you can’t do FunList.append(...) or FunList.remove(...) because those are list methods, not dict methods.