In this project, I want to use python classes in order to construct an emaginary box. For simplicity, I assume that the box can be specified using two points: the point at top-left of the box, and the other point at the down-right of the box. Also, in the second part of the project, the goal is to use the class Box to define a subclass with some extra features. This project, enables us to practice:
__dic__
as a dictionary that keep track of attributes of instances,As I mentioned, the box in our example must be represented using two points of top-left and down-right.
So we define a class named Box, with an initialising method who accept the coordinates of the points. However, suppose after creating the class, someone else wants to use it, and they do not have any instractions on how they need to pass arguments to the class. For instance, they do not know if they should pass coordinates simply one by one like Box(x1,y1,x2,y2)
, or if they should put coordinates of each node in tuples as Box((x1,y1),(x2,y2))
.
In order to avoid such kind of problems, I use the prefix asterics operator to enter arbitrary number of agruments.
The other important thing to consider is that the user may not have any clue about the order of entering the coordinates; they should first enter the coordinates of the top-left point or the down-right point. We can avoid such kind of conflicts by obliging the user to enter the arguments, by specifying their names. For instance, the user should enter x1 = 2 ,y1 = 1, x2 = 3, y2 = 4
or p1 = (2,1) , p2 = (3,4)
.
This can be done by using prefix operators **
in the definition of __init__
method of class Box. It declare to the __init__
method that there are an arbitrary number of arguments that can be entered with their names. In fact, the prefix asterisk operator, do a kind of unpacking process over the data entered to the function. For more information about it take a look at the unpacking concept in here.
In the snippet below **coordinates
signals the __init__
method that there are an arbitrary number of arguments which are accompanied by the keywords. As we know, the __dict__
dictionary is responsible to keep track of all of the attributs of instances of class, so one way to specify the attributes of the class is to update the __dict__
for each instance which is created. This is why I have used self.__dict__.update(**coordinates)
in the init method.
Note that, in the code **coordinates
represents that there are arbitrary number of keyword, arguments that can be entered as arguments, and they will be unpacked in a dictionary.
For instance, if we initialize the instance box
using box=Box(x1=2,x2=3,y1=1,y2=4)
it creates the dictionary coordinates={'x1': 2, 'x2': 3, 'y1': 1, 'y2': 4}
. So, in the first conditional statements, when there are four arguments we assume that their keywords should be x1
,x2
,y1
and y2
as indicated in keyorder
. I do the same for the case that the number of entered arguments are two: len(coordinates)==2
.
Since the main idea is to represent the instances with a specific order of coordinates (for instance when there are four arguments first I want to represent x1,y1 and then x2 and y2), I have costumized the sorted method of the dictionary using the lambda expressions in order to sort the entered data based on the predefined order of arguments in keyorder
.
In order to have a nice representation of the instances of type Box
, I also define the special method __str__
which is recalled when we use the str
function or print
function with the object.
class Box:
__counter = 0
sortedDic={}
def __init__(self, **coordinates):
type(self).sortedDic={}
self.__dict__.update(**coordinates)
if len(coordinates)==4:
keyorder= ["x1","y1","x2","y2"]
type(self).sortedDic=dict(sorted(self.__dict__.items(), key = lambda i: keyorder.index(i[0])))
if len(coordinates)==2:
keyorder=["p1","p2"]
type(self).sortedDic=dict(sorted(self.__dict__.items(), key = lambda i: keyorder.index(i[0])))
def __str__(self):
if len(self.__dict__ ) == 4:
return '({},{}),({},{})'.format(self.__dict__['x1'],self.__dict__['y1'],self.__dict__['x2'], self.__dict__['y2'])
elif len(self.__dict__) == 2:
return '{},{}'.format(self.__dict__['p1'] , self.__dict__['p2'])
In the following I have defined two instances of Box one using four argument and the other one with two arguments of type tuple.
b1 = Box(x1= 1 ,x2 = 3 , y1 = 2, y2 = 4)
str(b1)
'(1,2),(3,4)'
b2 = Box (p1 = (1,3) , p2 = (4,5))
str(b2)
'(1, 3),(4, 5)'
To improve our class Box
it would be nice to define an attribute for the area of the Box. This attribute should have be defined based on the arguments that are entered as arguments to the __init__
method. To implement this attribute, I am going to use descriptor protocols.
class Areagetter:
def __get__(self , obj , objType = None):
print('Calculate the Area of box')
if len(obj.__dict__) == 4:
return abs((obj.__dict__['x1'] - obj.__dict__['x2']) * (obj.__dict__['y1'] - obj.__dict__['y2']))
elif len(obj.__dict__) == 2:
return abs((obj.__dict__['p1'][0] - obj.__dict__['p2'][0])\
* (obj.__dict__['p1'][1] - obj.__dict__['p2'][1]))
def __set__(self, obj, value):
raise TypeError('It is not possible to change the area')
class Box:
sortedDic={}
Area = Areagetter()
def __init__(self, **coordinates):
type(self).sortedDic={}
self.__dict__.update(**coordinates)
if len(coordinates)==4:
keyorder= ["x1","y1","x2","y2"]
type(self).sortedDic=dict(sorted(self.__dict__.items(), key = lambda i: keyorder.index(i[0])))
if len(coordinates)==2:
keyorder=["p1","p2"]
type(self).sortedDic=dict(sorted(self.__dict__.items(), key = lambda i: keyorder.index(i[0])))
Area = Areagetter()
def __str__(self):
if len(self.__dict__ ) == 4:
return '({},{}),({},{})'.format(self.__dict__['x1'],self.__dict__['y1'],self.__dict__['x2'], self.__dict__['y2'])
elif len(self.__dict__) == 2:
return '{},{}'.format(self.__dict__['p1'] , self.__dict__['p2'])
In the above snippet, the class Areagetter is the descriptor which is used to define the attribute Area in the class Box
. In below, I have defined another instance of Box, and I use the attribute Area
to get its area.
b = Box(x1 = 5 , x2 = 10 , y1 = 8 , y2 = 9)
str(b)
'(5,8),(10,9)'
As you can see, the area of the box b
is 5.
b.Area
Calculate the Area of box
5
Note that in the definition of __set__
method in descriptor, I have raised an error because it is not logical to put a value to the attribute Area without changing the coordinates. As you can see below assigning a value to attribute Area, it rases an error.
b.Area = 7
--------------------------------------------------------------------------- TypeError Traceback (most recent call last) <ipython-input-78-6b2789b1afce> in <module> ----> 1 b.Area = 7 <ipython-input-74-68386f295197> in __set__(self, obj, value) 9 10 def __set__(self, obj, value): ---> 11 raise TypeError('It is not possible to change the area') 12 13 class Box: TypeError: It is not possible to change the area
In this step the idea is to define a simple subclass of the Box
class, which may have one extra attribute text
. The code that I have written below, take care of the existance of the text attribute in the arguments entered to the class, and act accordingly.
from copy import deepcopy
class TextBox(Box):
sortedDic={}
def __init__(self,**arguments):
#type(self).sortedDic={}
argument = deepcopy(arguments)
if "text" in arguments.keys():
text = argument.pop("text", None)
super().__init__(**argument)
self.__dict__["text"]= text
keyorder=["x1","y1","x2","y2","text"]
type(self).sortedDic= sorted(self.__dict__.items(), key=lambda i: keyorder.index(i[0]))
else:
super().__init__(**argument)
b_text = TextBox(x1=2,x2=3,y1=1,y2=4,text="Box1")
b_text.Area
Calculate the Area of box
As you can see, know we face a problem. The problem is that b_text
inherit the attribute Area, however it does not compute anything. However, when we define am instance of TextBox
without text argument, it canculate the Area correctly.
b_no_text = TextBox(x1 = 2 , x2 = 3 , y1 = 1 , y2 = 4)
b_no_text.Area
Calculate the Area of box
3
This problem occures because of the conditional statements in the definition of descriptor. In fact, adding the attribute text means that the length of __dict__
whould be either 3
or 5
, while these two cases do not considered in the definition of descriptors. By changing the conditional statements, this problem can be easily solved.