Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Some guidelines to improve code quality #3

Closed
nechepurenkoN opened this issue Jul 22, 2020 · 25 comments · Fixed by #5
Closed

Some guidelines to improve code quality #3

nechepurenkoN opened this issue Jul 22, 2020 · 25 comments · Fixed by #5
Assignees
Labels
help wanted Extra attention is needed

Comments

@nechepurenkoN
Copy link

nechepurenkoN commented Jul 22, 2020

Hello!
I've checked out your project and I want to suggest you some refactoring tips

  1. Make a MainWindow class and simply avoid using global variables
class MainWindow(Tk):
  styler = ttk.Style()
  storage = ...
  current_left = ...

  ....

and you can configure styles and other gui things in init method, for ex.:

  ...
  def __init__(self):
    super().__init__()
    self.title("PyCalc")
    ...
    styler.theme_use("calm")
    ...

and next you can use storage or temporary operand variables in any method here or directly pass it in other objects, for ex.:

  def put_in_storage(): # cout
    storage[-1] = ...
    ... 

This step needs to separate the gui functionality and the core program logic so you can independently change them.

  1. Define a class with all control elements
class Controls:
  operators = [....]
  special = [...]
  ...

and make an instance of this class a field of your MainWindow class. Using this approach you can easily bind control elements like:

  def control_binding():
    for control_element in [str(x) for x in range(10)]: # or your controls list
      some_button= ttk.Button(keypad, text=control_element, command=lambda: cout(control_element))
      # or append this buttons to button list and like button_list.append(some_button)
    

instead of:

nine = ttk.Button(keypad, text='9', command=lambda: cout('9'))
eight = ttk.Button(keypad, text='8', command=lambda: cout('8'))
seven = ttk.Button(keypad, text='7', command=lambda: cout('7'))
six = ttk.Button(keypad, text='6', command=lambda: cout('6'))
five = ttk.Button(keypad, text='5', command=lambda: cout('5'))
four = ttk.Button(keypad, text='4', command=lambda: cout('4'))
three = ttk.Button(keypad, text='3', command=lambda: cout('3'))
two = ttk.Button(keypad, text='2', command=lambda: cout('2'))
one = ttk.Button(keypad, text='1', command=lambda: cout('1'))
zero = ttk.Button(keypad, text='0', command=lambda: cout('0'))

I think you would got a point.

  1. Make a class with calc methods
    This is another step to separate your program components. Also you can make an instanse of this class as a field of MainWindow to have a direct access.

  2. Elimenate nested "if" instructions
    For ex.:

if cond1:
  if cond2:
    if cond3:
     ...
    else:
  ..elses..

can be written as:

if not cond1:
   ...
   return
if not cond2:
  ...
  return

I think you would got a point.

I recommend you to read "Clean code" by Robert Martin. There are a lot of guidelines to improve your code quality with perfect argumentation.

Good luck!

@tsadarsh
Copy link
Owner

This is a lot of very useful tips you have provided. The code badly needed a clean-up, but I did not know where to start. I was thinking about making it modular a.k.a separating the code into many sub-files and importing them when necessary. Your approach seems promising 👍 .

Defining a function to replace the repetition of code should be the first priority. It greatly help readability. Sparse is better than dense should be taken into account.

Thanks a bunch for this detailed Issue. I will get back to you, if I land up in any trouble while re-writing the code. I also would like to know about your views on breaking the code to many sub-files instead of one long source.py. Will it be a better approach?

@nechepurenkoN
Copy link
Author

Definetely, it will be.

You can make a folder (package) for graphical things like windows (for ex. SimpleMainInterface and AdvancedMainInterface) and for styling purposes (for ex. make your own buttons by inheriting from tk buttons with future customizations). Also make a core package with a class for calculation. The idea is to separate gui and core functionality for independent testing and development.
It is a part of weak connection concept.

I see the file hierachy as

__init__.py
main.py
gui
  windows
    abstractwindow.py
    simpleinterfacewindow.py
    advancedinterfacewindow.py
  buttons
    custombuttons.py
core
  calculation
    expressionparser.py
    calculator.py
  controls
    operations.py
    functions.py
    ...
tests
  ....

You need to make your windows implementing singleton pattern for some reasons. You can read about it here: singleton. It would help you to store the state of your window.

Ask me if you have any questions and keep developing!

@tsadarsh
Copy link
Owner

In the process of binding control elements,

  def control_binding():
    for control_element in [str(x) for x in range(10)]: # or your controls list
      some_button= ttk.Button(keypad, text=control_element, command=lambda: cout(control_element))
      # or append this buttons to button list and like button_list.append(some_button)

To grid all the buttons in columns of three, some_button is appended to button_list and then in def gridding the buttons are accessed using indices, am I right?

# earlier code (code to be refactored)
nine.grid(row=2, column=0)
eight.grid(row=2, column=1)
seven.grid(row=2, column=2)
six.grid(row=3, column=0)
five.grid(row=3, column=1)
four.grid(row=3, column=2)
three.grid(row=4, column=0)
two.grid(row=4, column=1)
one.grid(row=4, column=2)
# refactored code
...
    def control_binding(self):
        for control_element in [str(number) for number in range(10)]:
                    some_button = ttk.Button(keypad, text=control_element, command=lambda: self.cout(control_element))
                    self.NUM_BUTTONS.append(some_button)

    def gridding(self):
        ROW = 1
        for button_place in range(len(NUM_BUTTONS):
            MOD =   button_place%3
            if MOD == 0:
                ROW += 1
            NUM_BUTTONS[button_place].grid(row=ROW, column = mod)
...

Is this the correct approach to do it?

@nechepurenkoN
Copy link
Author

Exactly!

In the case of number buttons your can shorten gridding code as

def gridding(self):
    for button_index in range(len(num_buttons)):
         num_buttons[button_index].grid(row=button_index//3, column=button_index%3) # or row=button_index//3 + 2 if you need to start from 2

Most programming languages have own code style guides/naming conventions. In python the most considered style guide is PEP-8. According to this all uppercase variable names should refer to constant objects, for ex.: DAYS_PER_WEEK = 7 or GPU_CORES = 4 etc. Try to avoid using uppercase names when its not a constant object (like num_button or row/mod).

@tsadarsh
Copy link
Owner

tsadarsh commented Jul 24, 2020

Nested if-else is a real mess to clear. Unfortunately, there is a bunch of those in core logic part of code. To make the nested statements flat, usage of and and or operators the correct approach?
Following is one of many nested blocks:

...
    if char in OPERATORS:
        if STORAGE[-1] == None:
            if char in OPERATORS[:2]:
                ''' Case when first input is '/' or '*' '''
                STORAGE += ['1']+list(char)
            else:
                ''' Case when first input is '+' or '-' '''
                STORAGE += ['0']+list(char)
        elif STORAGE[-1] in OPERATORS:
            ''' Switching to latest operator, example: '3+-' evalutes to '3-' '''
            STORAGE[-1] = char
        else:
            STORAGE += list(char)
    else:
        ...
...

The next following code is an attempt to make the nested block flat:

...
# if block
if char in OPERATORS and STORAGE[-1] is None and char in OPERATORS[:2]:
	''' Case when first input is '/' or '*' '''
	STORAGE += ['1']+list(char)

if char in OPERATORS and STORAGE[-1] is None and char not in OPERATORS[:2]:
	''' Case when first input is '+' or '-' '''
	STORAGE += ['0']+list(char)

if char in OPERATORS and STORAGE[-1] in OPERATORS:
	''' Switching to latest operator, example: '3+-' evalutes to '3-' '''
	STORAGE[-1] = char

if char in OPERATORS and STORAGE[-1] not in OPERATORS:
	STORAGE += list(char)

# else block
if char not in OPERATORS and ...
...

@tsadarsh tsadarsh self-assigned this Jul 24, 2020
@nechepurenkoN
Copy link
Author

Logic operators are good to use in conditions, but the idea of flattening is to make represented conditionals more readable.
Your eyes should flow by your code (? english is not my native, idk how to shape it). All conditional logic should be easy to understand.

Some ideas are below. It is not a perfect solution, but compare it to your version. Is it easier to understand?

class Storage:
    __operators: list = ["*", "/", "-", "+"]
    __special: list = ["C", "AC", "P/M"]
    __storage: list

    def __init__(self):
        self.__storage = []

    def get_storage(self) -> list:
        """ get storage as a list """
        return self.__storage

    def get_storage_as_string(self) -> str:
        """ get string representation """
        return "".join(
            map(
                lambda element: " " + element + " " if element in self.__operators else element,
                self.__storage
            )
        )

    def __repr__(self) -> str:
        return self.get_storage_as_string()

    def put_in_storage(self, character: str) -> None:
        """ puts a character to storage """
        if character in self.__operators:
            return self.__put_operator(character)
        if character in self.__special:
            return self.__apply_special(character)
        if not character.isnumeric() and character != ".":
            raise Exception("Given symbol is invalid")  # todo: check if there is only one dot in number
        return self.__put_digit(character)

    def __put_operator(self, character: str) -> None:
        if len(self.__storage) > 0:  # is not empty
            if self.__storage[-1] in self.__operators:
                self.__storage[-1] = character
            else:
                self.__storage.append(character)
        else:
            """ put "1" + ("/" or "*") or "0" + ("+" or "-") """
            self.__storage.extend([["1", "0"][self.__operators.index(character)//2], character])

    def __apply_special(self, character: str) -> None:
        if character == "AC":
            self.__storage.clear()
        if character == "C":
            pass
        if character == "P/M":
            pass

    def __put_digit(self, character: str) -> None:
        if len(self.__storage) == 0 or self.__storage[-1] in self.__operators:
            """ is empty or previous chunk is an operator """
            self.__storage.append(character)
        else:
            self.__storage[-1] += character


def test1(storage_instance: Storage) -> tuple:
    storage_instance.put_in_storage("1")
    storage_instance.put_in_storage("+")
    storage_instance.put_in_storage("2")
    storage_instance.put_in_storage("4")
    storage_instance.put_in_storage("/")
    storage_instance.put_in_storage("5")
    result_str = storage_instance.get_storage_as_string()
    return result_str, 1 if result_str == "1 + 24 / 5" else 0


def test2(storage_instance: Storage) -> tuple:
    storage_instance.put_in_storage("/")
    storage_instance.put_in_storage("5")
    result_str = storage_instance.get_storage_as_string()
    return result_str, 1 if result_str == "1 / 5" else 0


def test3(storage_instance: Storage) -> tuple:
    storage_instance.put_in_storage("+")
    storage_instance.put_in_storage("1")
    result_str = storage_instance.get_storage_as_string()
    return result_str, 1 if result_str == "0 + 1" else 0


if __name__ == "__main__":
    # make unit tests using unittest?
    for test in [test1, test2, test3]:
        storage_instance = Storage()
        test_result = test(storage_instance)
        print("{}: got {}, valid? {}"
              .format(test.__name__, test_result[0], ["not", "yes"][test_result[1]]))

@nechepurenkoN
Copy link
Author

I know that it might be a bit complicated code for beginner, so if you have any questions feel free to ask me here.

@tsadarsh
Copy link
Owner

@nechepurenkoN, to be honest, the concepts of tests and using of class is something I have never implemented myself. They remain solely in tutorial examples. All of your suggestions are forcing me to learn concepts which I either ignored or which I have never encountered myself. I do foresee a lot more queries to be posted by me here.

Thanks a lot for lending a helping hand. Your help and suggestions means a lot to me. You will be bothered many more times before the completion of this refactoring project 😄 .

@nechepurenkoN
Copy link
Author

The truth is if you want to become a software developer, one day you will have to face up classes, tests and other boring stuff.

Why these classes, refactoring, decomposition etc. are important?
If you want to build a good house you need a solid foundation. Even if you want to build a hut of branches.
A good code architecture and styling make new features or changes easier to implement. It provides you an opportunity to update some functionality without affecting stable code what is a very important thing if your program grows.

It might be like a steep trampoline, but any difficulties your overcome make you more experienced and professional.
As you develop this habit to map out what kind of architecture should have been implemented, what components of your program should be packaged as classes etc., you elaborate your professional vision of problem. And this pet-project can become like a training camp where you can practice and apply new knowledge, why not?

I'm not bothered to help you because I answer in my spare time and I don't need something in return. Don't be ashemed to ask questions that seems "fool" or something like that, because it's normal when you learn something new. Programming concepts like object-oriented paradigm are not easy to understand on your own, but I can help you with examples related to your project.
Like in the code above, I haven't changed the main logic itself, but I've reorganized code to make it like an indepent component, so it could be reused and modified without affecting other "components".

I don't insist to do this refactoring things, but if you want to, mention me at this thread with any question you have.

@tsadarsh
Copy link
Owner

tsadarsh commented Jul 25, 2020

I don't insist to do this refactoring things, but if you want to, mention me at this thread with any question you have.

I have been refactoring this code, right from the day this thread was opened. I made little progress due to lack of experience and clarity. Your previous snippet of refactored code is just what I needed. I will update my progress regularly is this separate gist.

Some ideas are below. It is not a perfect solution, but compare it to your version. Is it easier to understand?

It is way easier to understand the code now. It is very clear to me now that architecture of the code is very important for readability.

I'm not bothered to help you because I answer in my spare time and I don't need something in return.

This is so nice of you. 😄

I know that it might be a bit complicated code for beginner, so if you have any questions feel free to ask me here.

TBH, it looked complicated at first sight. I sat down yesterday and started exploring all the unknowns concepts in your example code. End result, I was able to see the beauty of your refactored chunk of code after learning function annotation, variable annotation, difference between class variable and instance variable.

I believe continuing to keep this thread as descriptive as possible will help other new-devs in future in some or the other way. 🤓

@tsadarsh
Copy link
Owner

Update from WIP refactor source gist.
I made a class for calculation, calculate. To link both the classes Storage and Calculate, I first made an instance obj in go_for_calc. In class Calculate expression holds string representation of __storage:

class Storage:
    ...
    
    def go_for_calc(self):
        obj = Calculate(self.__storage)
        self.__answer = obj.__calculate
    ...

class Calculate:
    ans: str

    def __init__(self, expr_as_list):
        self.expression = ''.join(i for i in expr_as_list)

    def __calculate(self) -> str:
        self.ans = str(eval(self.expression)
        return self.ans

Unfortunately, this throws AttributeError: 'Calculate' object has no attribute '_Storage__calculate'. I don't understand why this does not work. I did a workaround like this:

class Storage:
    ...
    
    def go_for_calc(self):
        obj = Calculate(self.__storage)
        self.__answer = obj.ans
    ...

class Calculate:
    ans: str

    def __init__(self, expr_as_list):
        self.expression = ''.join(i for i in expr_as_list)
        self.__calculate()

    def __calculate(self) -> str:
        self.ans = str(eval(self.expression)

This does give the desired answer but I am pretty sure there is a better way to do this.
Note: I have used eval() to parse the equation as a temporary use. Calculation logic is to be added later in class Calculate.

@nechepurenkoN
Copy link
Author

nechepurenkoN commented Jul 25, 2020

In the line 56 there is a typo:

self.__storage.extend(['0', '1'][self.__operator.index(operator)//2, operator])

instead of

self.__storage.extend([['1', '0'][self.__operator.index(operator)//2], operator])

It needs ']' after '//2', "1" before "0" and "[" in the beginning. How does it work?
Extend method make something like a concatenation but operating with lists, so in this line we are trying to concatenate our storage and a list of 2 values. The second value is the operator, but what is the first? Here is some magic:

# we define a temporary list ["1", "0"]; for simplicity, let it be:
a = ["1", "0"] 
# how can we get 1 if operators are "*" or "/" and 0 if operators are "+", "-" ?
# we know, that this character IS in our operators list, so lets find its position:
index = self.__operators.index(character)
# we will get index in [0..3] range, and 0-1 should be mapped to 0th index in a, 2-3 to 1th index in a
# so we divide index by 2
index_in_a = index // 2
# and get the number
result = a[index_in_a]
# hence, we can shorten this expression, by defining list "a" in place and not assigning it to a variable
# [create a][get index_in_a]
  ["1", "0"][self.__operators.index(character)//2] == ("1" if character in ["/", "*"]) or ("0" if character in ["+","-"])

This is not a good practice make things like that, but I wanted to show you some language sugar :)

@nechepurenkoN
Copy link
Author

nechepurenkoN commented Jul 25, 2020

Two underscores mark a class field or method as private. It means that you can access it only in class definition, for ex.:

class A:
  __a = 10

class B:
  def __init__(self):
    a_instance = A()
    print(a_instance.__a) # is incorrect, because the field __a is private
                   # it cannot be accessed outside class A.

Private functions also are not accessible outside class definition. There is a way to access it, but it is some kinda hacking.
If you want to function or field being visible outside then don't use two underscores in the name prefix. For ex.:

class A:
  def calculate(self):
    pass
  def __calculate1(self):
    pass

class B:
  def foo(self):
    a = A()
    a.calculate() # all good
    a.__calculate1() # error

Why should you use private functions then? Let's look up in put_in_storage. It is a public function, we can call it outside a class definition. We can create an object (instance) and call this method to put character in storage. But why __put_operand is private? What will happen if we make it public? You could call storage_instance.put_operand("1") or storage_instance.put_operand("&") and it is a bad news. It will break a core logic, because we secure this function and call it if only character is a valid operand, in other words "+", "-", "*" or "/".

If it is not clear ask me for another examples.

@tsadarsh
Copy link
Owner

Two underscores mark a class field or method as private. It means that you can access it only in class definition

Ok,
I tried out and found that inherited class does not come under the base class definition. I need the __operators defined in class Storage to be included in class Calculate. To do this I found two ways to do it:

class Calculate(Storage):
    operators = Storage.__operators

    def __init__(self, expr_as_list):
        self.expression = ''.join(i for i in expr_as_list)
    ...

In this second approach I made the following changes in Storage and Calculate:

class Storage:
    ...
    def go_for_calc(self):
        obj = Calculate(self.__storage, self.__operators)
        ...
    ...

class Calculate:
    ...
    def __init__(self, expr_as_list, operators):
        self.expression = ''.join(i for i in expr_as_list)
        self.operators = operators
    ...

@nechepurenkoN, can you please point out which is safer way, or if there is a better way to do it?

@nechepurenkoN
Copy link
Author

nechepurenkoN commented Jul 26, 2020

IMHO, the second way is better. Inheritance is used when you want to extend existing class with new fields/methods or redefine methods. For example, there is Hero class and its objects have attack() method. You can make knight and archer class and redefine attack method.

class Hero:
  def __init__(self, base_damage):
    self.base_damage = base_damage
  def attack(self):
    pass

class Knight(Hero):
  def __init__(self, base_damage):
    super().__init__(base_damage)
  def attack(self):
    print(f"Melee attack with {self.base_damage} damage")

class Archer(Hero):
  def __init__(self, base_damage):
    super().__init__(base_damage)
  def attack(self):
    print(f"Range attack with {self.base_damage} damage")

Knight and archer are heroes, so it is logical to use inheritance. But Storage and Calculator are not so connected.

Usually, all the class fields are made private. As I said, if we make self.__storage of Storage as self.storage, then you (or other developers) could make some bad things:

storage_instance = Storage()
storage_instance.put_in_storage("&") # will not have a bad consequences
storage_instance.storage.append("&") # some problems here

So we have to protect storage list -> make it private (__storage). And the question itself: how to access this field?
Let's introduce getters and setters methods:

class A:
  def __init__(self):
    self.__a = 10
    self.__b = 5
  def get_a(self):
    return self.__a
  def get_b(self):
    return self.__b
  def set_a(self, new_a):
    self.__a = new_a
  def set_b(self, new_b):
    self.__b = new_b

obj = A()
print(obj.get_a())
obj.set_a(42)
print(obj.get_a())

But in our case, we cannot just make def get_storage(self): return self.__storage, because it is a list object and we could break it again:

storage_list = storage_instance.get_storage()
storage_list.append("&") # would affect __storage (see how does python store lists)

Solution:

...
def get_storage(self):
  return self.__storage.copy()
...

We return a copy of storage list which we can read and manipulate with. Make a method in Calculator

...
def calculate(self, storage: Storage) -> float:
  storage_list = storage.get_storage()
  # or use storage.get_storage_as_string
  ...

Usage:

storage_instance = Storage()
calculator_instance = Calculator()
result = calculator_instance.calculate(storage_instance)

@nechepurenkoN
Copy link
Author

There is a way to shorten calculation code. I adapt it to your method.

operators = ["*", "/", "+", "-"]
expression_list = ["1", "+", "2"]

functional_mapping = {
    "+": lambda x,y: x+y,
    "-": lambda x,y: x-y,
    "*": lambda x,y: x*y,
    "/": lambda x,y: x/y
}

left_operand = float(expression_list[0])
right_operand = float(expression_list[2])
operator = expression_list[1]

result = functional_mapping[operator](left_operand, right_operand)
print(result)

When we call funtional_mapper[operator], for example: functional_mapper["+"], we get an assigned lambda function (function without name and with only omitted return statement) and we can pass our operands in this function. Try to test this code. And to add new operation just add in functional_mapper new line like: "@": lambda x,y: (return) ...

@tsadarsh
Copy link
Owner

When we call funtional_mapper[operator], for example: functional_mapper["+"], we get an assigned lambda function (function without name and with only omitted return statement) and we can pass our operands in this function. Try to test this code. And to add new operation just add in functional_mapper new line like: "@": lambda x,y: (retur

This is absolute beauty. I really like this logic to shorten the code 👍.

        if not character.isnumeric() and character != ".":
            raise Exception("Given symbol is invalid")  # todo: check if there is only one dot in number

Update from gist, when multiple decimal(dot) entry, logic updated to consider only first entry (ln: 79).

    def __put_dot(self, dot):
        if self.__storage[-1] not in self.__operators:
            index_of_dot = self.__storage[-1].find(dot)
            if index_of_dot != -1:
                ''' Multiple decimal inputs ex! (3.2.).
                Replacing '''
                self.__storage[-1].replace(dot, '')
            else:
                self.__storage[-1] += dot 

It is still disturbing to see the need of using nested if-else here. I don't see a workaround to solve this without a nested block.

@nechepurenkoN
Copy link
Author

If there was a dot before, why don't just ignore next number dots? This is how Windows calculator works. Test out, what happens if you are trying to add another dot to a number (in Linux for ex.)?

To ignore other dots simply add a condition

def __put_dot(self):
  if len(self.__storage) == 0:
    self.__storage.append("0.")
    return
  if self.__storage[-1] not in self.__operators and not "." in self.__storage[-1]:
    self.__storage[-1] += "."

@tsadarsh
Copy link
Owner

Test out, what happens if you are trying to add another dot to a number (in Linux for ex.)?

Linux points out where the malformed expression is:

calc decimal

@nechepurenkoN
Copy link
Author

I think that ignoring is not a bad decision at all

@tsadarsh
Copy link
Owner

Today's update:

  • Function __put_dot updated.
  • New class for GUI added.
  • Fatal errors in GUI:
    • Every button entry (excluding '=') results in display of character '0'

I came to a conclusion that ttk.Button with same name rebinds the command to latest definition. Hence this code chunk rebinds every button command to '0' (last entry):

...
    __layout = ['*', '/', 'C', 'AC',
                '9', '8', '7', '-',
                '6', '5', '4', '+',
                '3', '2', '1', '+/-',
                '.', '0', '=']
    ...
# ln: 170
        ''' Control Binding '''
        buttons = []
        for bt_text in self.__layout[:-1]:
            bt = ttk.Button(master=keypad)
            bt['text'] = bt_text
            bt['command'] = lambda: (self.bt_cmd.into_storage(bt_text),
                                     self.bt_cmd.show_storage(self.DISPLAY))
            buttons.append(bt)
...

Is this conclusion relevant or am I getting it wrong?

@tsadarsh
Copy link
Owner

Why do we refactor a code? To make the code shorted or to make it more readable?
If the answer is to achieve best of both worlds; if a case arises where a certain code chunk can either be made made readable-compromising the code length- or can be made shorter -compromising the readability- which option should one choose?

@nechepurenkoN
Copy link
Author

nechepurenkoN commented Jul 30, 2020

I really have no idea why doesn't buttons binding work :(. I haven't used python gui libraries so I can't help you with previous question.

If you can make your code more readable, then make it, but keep in mind that some programmers are more experienced than we are. They don't use, for example, range loops, generally speaking - common approaches. The way they shorten the code is using some language technics, sugar etc. without making it more difficult to read for people who knows the language as intermediate user. For example:

a = [1,2,3,4]
for i in range(len(a)):
  print(f"index: {i}, element: {a[i]}")

for index, element in enumerate(a):
  print(f"index: {index}, element: {element}")
a = []
for i in range(5):
  if i % 2 == 0
   a.append(i*i)

a = [i*i for i in range(5) if i % 2 == 0]
# consider a, b are int variables
temp = a
a = b
b = temp

a, b = b, a

We make our code more readable by using built-in function enumerate. The first approach is more general, it might be used in any programming language with arrays and for-loops. The second one is using a generator - some useful pythonic feature. Readability is the most important thing, but if if you can shorten the code without losing readability then make it.

@tsadarsh tsadarsh added the help wanted Extra attention is needed label Jul 31, 2020
@tsadarsh tsadarsh mentioned this issue Aug 1, 2020
@tsadarsh
Copy link
Owner

tsadarsh commented Aug 1, 2020

Presently there seems no better workaround for button binding problem. Anyhow, source.py is refactored and broken down to separate files.
@nechepurenkoN many thanks for bringing up this issue 🎉 . The code now looks way cleaner and readable than before, I believe. To sum up all the changes done:

  • All usage of global scrapped,
  • seperate classes for calculation, gui, storage,
  • deeply nested if-else flattened.

@tsadarsh tsadarsh reopened this Aug 15, 2020
@tsadarsh
Copy link
Owner

I have thinking about this for a while: using a Entry widget in place of Label -(display_label)- widget to display the input/result seems more appropriate. By switching to a Entry field:

  • there is no need for key-bindings,
  • no need to explicitly trim the display to specified characters.

Moreover it morally doesn't seem right. Here are some words from tkdocs:

A label is a widget that displays text or images, typically that the user will just view but not otherwise interact with. Labels are used for such things as identifying controls or other parts of the user interface, providing textual feedback or results, etc.

Entry widget:

An entry presents the user with a single line text field that they can use to type in a string value.

Allowing users to directly enter input (ex: copy/paste) can be achieved by using ttk.Entry widget too. Is this switch-over worthy, maybe a good time to start a separate issue to address this subject?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
help wanted Extra attention is needed
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants