I have Python a Algorithm
class with a calculate
method that takes the list of tuples where every tuple have 2 integers (coordinates of the dots) and goal
argument and returns integer - a number of lines that have more than int(goal) - 1
dots on it.
For example, if we have four dots on one line (like . . . .
, with coordinates [(0, 0), (1, 0), (2, 0), (3,0)]
) and goal
is 3
, then the algorithm must return 1, instead of 4.
I have a Python-Tkinter UI that has a field with dots. A user can choose some of them, and program must show points
for currently chosen dots after every new click (so speed matters).
""" This module contains algorithms.
Dot, Vector and Chain classes represents objects, while Algorithm
class provides just an interface for use.
See `Dot.build_vectors` and `Dot.check_connection` for understanding. """
from settings import GOAL
from .logger import calc_lg
# =================
# helpful-functions
# =================
def greatest_common_divisor(a, b):
""" Euclid's algorithm """
while b:
a, b = b, a % b
return a
def _check_type(obj, *expected) -> object:
if type(obj) not in expected:
raise RuntimeError('Expected type: "{}", got: "{}".'.format(type(obj), expected))
return obj
# =======
# Classes
# =======
class Dot:
""" Represents dots on the xOy (e.g. checkers on the field). """
dots = []
""" Field for storing all instantiated `Dot` objects. """
build_all_lines = False
def __init__(self, x: int, y: int):
self.x = x
self.y = y
self.vectors = {}
self.chains = []
# to not use custom `register` method as for `Chain` and `Vector`
self.dots.append(self)
def __repr__(self):
return '<Dot x:{} y:{}>'.format(self.x, self.y)
def build_vectors(self):
""" Builds vectors to all other dots (that are in `dots` field). """
for dot in self.dots:
if dot != self: # if it is another dot
vector = Vector(self, dot).register() # add vector to storage
self.check_connection(vector, dot)
def check_connection(self, vector, dot):
""" Checks if `self` have connection to this `dot` by this `vector`.
If have, than creates new Chain, else store it in `vectors` dictionary.
**Note**: this method is the most important """
_check_type(vector, Vector)
_check_type(dot, Dot)
if vector in self.vectors: # case when we have `dot` for this `vector`
# create `Chain` object with 3 dots inside:
# 1. `self`, 2. dot associated by `vector` 3. `dot`
Chain(vector, self).add(self.vectors[vector]).add(dot).register()
else:
# add `dot` to the dictionary with `vector` as key, to
# have access to this later
self.vectors[vector] = dot
if self.build_all_lines: # ! (4-question point) !
Chain(vector, self).add(dot).register()
class Vector:
""" Represents connections between Dots (e.g. lines between checkers). """
vectors = []
""" Field for storing all vectors together. """
def __init__(self, dot1: Dot, dot2: Dot):
_check_type(dot1, Dot)
_check_type(dot2, Dot)
self.x, self.y = self.calculate_vector(dot1, dot2)
def __repr__(self):
return '<Vector x:{} y:{}>'.format(self.x, self.y)
def __hash__(self):
return id(self).__hash__()
def __eq__(self, other):
""" Compares vectors by their coordinates. Returns `True` if
they ar collinear, else - `False` """
return (self.x == other.x and self.y == other.y) or \
(self.x == -other.x and self.y == -other.y)
@staticmethod
def calculate_vector(dot1: Dot, dot2: Dot) -> tuple:
""" Returns pair of integers: x, y - coordinates of the vector
between given dots. """
_check_type(dot1, Dot)
_check_type(dot2, Dot)
x = dot1.x - dot2.x
y = dot1.y - dot2.y
gcd = greatest_common_divisor(max(x, y), min(x, y))
return int(x/gcd), int(y/gcd)
def register(self):
""" Checks if it is equal vector in `vectors` field.
If so - deletes `self`, else - appends `self` to `vectors`"""
for item in self.vectors:
if self == item:
del self # very dangerous place !!!
return item
else:
self.vectors.append(self)
return self
class Chain:
""" Represents groups of Dots connected with the same Vectors (e.g. checkers on one line). """
chains = []
""" Field for storing all Chain objects. """
def __init__(self, vector: Vector, dot1: Dot):
_check_type(vector, Vector)
_check_type(dot1, Dot)
self.vector = vector
self.dots = [dot1]
self.len = 1
def __repr__(self):
return '<Chain [Vector: {}, len: {}, first Dot: {}]>'.format(self.vector, self.len, self.dots[0])
def add(self, dot: Dot):
""" If `dot` isn't in `dots` attribute (list) - append it and
increment `len` attribute. """
if dot not in self.dots:
self.dots.append(dot)
self.len += 1
return self
def _check_in(self, other):
""" Checks if it is even one dot in both Chain objects.
If so - return `True`, else - `False`. """
_check_type(other, Chain)
for dot in self.dots:
if dot in other.dots:
return True
else:
return False
def _merge_in(self, other):
""" Appends all dots that aren't in `other` to `other`. """
_check_type(other, Chain)
for dot in self.dots:
if dot not in other.dots:
other.add(dot)
def register(self):
""" Check if it is equivalent chain in `chains` field.
Do comparing by calling `_check_in` method and comparing
vectors, and if they are equal - call `merge_in` and
deletes `self`, and returns `other`, else - return `self`. """
for item in self.chains:
if self.vector == item.vector and self._check_in(item):
self._merge_in(item)
del self
return item
else:
self.chains.append(self)
return self
class Algorithm:
def __init__(self):
self.Chain = Chain
self.Dot = Dot
self.Vector = Vector
def init_dots(self, array: list):
""" Initiate dots from coordinates in `array`."""
for x, y in array:
self.Dot(x, y)
def init_vectors(self):
""" Initiate Vectors by building connections from every dot. """
for dot in self.Dot.dots:
dot.build_vectors()
def clear(self):
""" Clear all initiated objects. """
# this definition must remove all references to
# created objects, so garbage collector must
# delete them for ever
self.Dot.dots = []
self.Vector.vectors = []
self.Chain.chains = []
def calculate(self, array: list or tuple, goal: int =GOAL, build_all_lines: bool =False, auto_correct: bool =True) -> int:
""" Calculates how main points you have.
Initiates all objects, counts points, logs result."""
points = ''
# checking input data
_check_type(goal, int)
_check_type(build_all_lines, bool)
_check_type(auto_correct, bool)
_check_type(array, list, tuple)
if goal == 2 and not build_all_lines:
if auto_correct:
calc_lg.debug('Cannot calculate when "build_all_lines" is "False".')
build_all_lines = True
calc_lg.debug('Auto-correction performed: set "build_all_lines" to "True".')
else:
msg = 'Cannot calculate when "build_all_lines" is "False" and "goal" is "2".'
calc_lg.error(msg)
raise RuntimeError(msg)
elif goal < 2: # we cannot build Chain with less then 2 dots inside
raise RuntimeError('Cannot calculate when "goal < 2".')
elif build_all_lines:
if auto_correct:
calc_lg.debug('Overhead: "build_all_lines" will increase calculation time, setting it to "False".')
build_all_lines = False
calc_lg.debug('Auto-correction performed: set "build_all_lines" to "False".')
else:
calc_lg.debug('Overhead: "build_all_lines" will increase calculation time')
else:
calc_lg.debug('Starting calculation.')
# actually calculation
try:
self.Dot.build_all_lines = build_all_lines
self.init_dots(array)
self.init_vectors()
points = self._get_points(goal)
except Exception as e:
calc_lg.exception(e, exc_info=True)
points = 'ERROR'
raise e
else:
calc_lg.info('Points - {} ; {}'.format(points, str(array)))
finally:
self.clear()
return points
def _get_points(self, goal: int) -> int:
""" Counts how many chains have more chan `goal` dots inside. """
points = 0
for chain in self.Chain.chains:
if chain.len >= goal:
points += 1
return points
algo = Algorithm()
See more:
- Version for question
- Tests for this version (I have built the project with idea to use it without downloadable Python packages, so this module is weird)
Questions:
- Is code formatting and documentation good, readable and easy to understand?
- Does this class implementation need improvement to meet some unknown for me common practices?
- I want
Algorithm.clear
method to clear memory and prepare for next calculations, but I'm not sure that it is the best way to do it. - To improve performance, I have implemented
build_all_lines
argument in theAlgorithm.calculate
method, because inDot.check_connection
at! (4-question point) !
if
statement depends on it. If this argument isTrue
, then the code is three times slower than without. Actually, theauto_correct
argument was added to auto-correctbuild_all_lines
argument depending on thegoal
argument. I'm not sure if it's okay.