Python Polymorphism - python

I have an assignment from an instructor trying to teach OOP in python. I'm pretty familiar with OOP in C++ and C# but having a difficulty figuring out what is going on in some of my code. It works and both classes function as I want but I had to add some weird code for it to work and I don't understand why.
Specifically referencing the code starting in line 64
class Cone(Cylinder):
#Constructor
def __init__ (self, radius, height):
Cylinder.__init__(self, radius, height)
self.calcVolume()
def calcVolume(self):
Cylinder.calcVolume(self)
self.__volume = Cylinder.GetVolume(self) * (1.0/3.0)
So when implementing Cone I don't understand why the Cylinder.calcVolume() is not called when the cone's constructor calls the cylinder's constructor. The code would make more sense to me if it was implicitly called but im forced to call the method explicitly. Some guidance or explanation would be awesome.
After Posting I made this change does it make more sense?
class Cone(Cylinder):
#Constructor
def __init__ (self, radius, height):
Cylinder.__init__(self, radius, height)
self.calcVolume()
def calcVolume(self):
self.__volume = self.GetBase().GetArea() * self.GetHeight() * (1.0/3.0)

This is what happens when you call Cone.__init__():
it performs Cylinder.__init__(),
in turns it calls self.calcVolume(),
because of inheritance, the resolution order finds the method on the Cone type,
it calls Cone.calcVolume() instead of Cylinder.calcVolume().
During __init__() I think you want to call:
Cone.calcVolume(self) in Cone.__init__(), or
Cylinder.calcVolume(self) in Cylinder.__init__().
Of course, if you were using new style classes (inheriting from object) then you could just use type(self).calcVolume(self); however type(self) on an old-style class is going to give you the instance type instead of the actual class, which isn't going to work in your case.
full example:
class Circle():
#Constructor
def __init__ (self, radius):
self.__radius = radius
self.calcArea()
def calcArea(self, PI = 3.14):
self.__area = (self.__radius**2) * PI
#Get Functions
def GetArea(self):
return self.__area
def GetRadius(self):
return self.__radius
#Set Functions
def SetRadius(self, radius):
self.__radius = radius
self.calcArea()
class Cylinder():
#Constructor
def __init__(self, radius, height):
self.__height = height
self.__base = Circle(radius)
Cylinder.calcVolume(self)
def calcVolume(self):
self.__volume = self.__base.GetArea() * self.__height
#Get Functions
def GetVolume(self):
return self.__volume
def GetBase(self):
return self.__base
def GetRadius(self):
return self.__base.GetRadius()
def GetHeight(self):
return self.__height
#Set Functions
def SetRadius(self, radius):
self.__base.SetRadius(radius)
self.calcVolume()
def SetHeight(self, height):
self.__height = height
self.calcVolume()
class Cone(Cylinder):
#Constructor
def __init__ (self, radius, height):
Cylinder.__init__(self, radius, height)
Cone.calcVolume(self)
def calcVolume(self):
Cylinder.calcVolume(self)
self.__volume = Cylinder.GetVolume(self) * (1.0/3.0)
#Get Functions
def GetVolume(self):
return self.__volume
#Set Functions
def SetRadius(self, radius):
Cylinder.SetRadius(self, radius)
self.calcVolume()
def SetHeight(self, height):
Cylinder.SetHeight(self, height)
self.calcVolume()
def main():
cylinder = Cylinder(5, 6)
cone = Cone(5, 6)
circle = Circle(5)
print cylinder.GetVolume()
print cone.GetVolume()
print circle.GetArea()
cone.SetHeight(7)
print cone.GetVolume()
main()

Rule is pretty straightforward: when the method is to be called, its name is resolved according to Method Resolution Order (it is tricky, as Python has multiple inheritance).
After the name is resolved, Python stops searching (I am simplifying). It is up to the specific method to call parent class's method (or any other method). Doing it implicitly could be a common cause of problems (eg. when you want to override some method from parent class).
Also, to invoke parent method use super(). Instead of writing this in Cone class:
Cylinder.__init__(self, radius, height)
write this:
super(Cone, self).__init__(self, radius, height)
That makes it more flexible: it just delegates finding proper method to MRO, which then checks parent classes in specific order (in this case, it will indeed find Cylinder.__init__).
More reading:
Guido Van Rossum on Method Resolution Order,
documentation of super() in Python 2.7,
"Python's super() considered super" - blog post about capabilities of super(),

Related

Python Trigger a function when a function is called

So, I am making a desktop app with PyQt6. I have a stylesheet and I want it to update every time I add something to it. Stylesheet is an array and I have multiple functions adding things to it:
def set_color(self, color):
self.stylesheet.append(f"color: {color}")
def set_background_color(self, color):
self.stylesheet.append(f"background-color: {color}")
def set_border_radius(self, radius):
self.stylesheet.append(f"border-radius: {radius}px")
def set_alignment(self, alignment):
self.stylesheet.append(f"text-align: {alignment}")
Now, I want to update the stylesheet every time I call one of these functions (or a function in the class as a whole) by calling another function that updates it. But I don't want to add it manually to every function. Is there a better way of doing this?
def update_stylesheet(self):
result = ""
for css in self.stylesheet:
if css.strip():
result += css.strip()
result += "; "
self.setStyleSheet(result)
Simple solution: Use function composition.
Instead of calling self.stylesheet.append you could make a helper method, that will call self.stylesheet.append and also call update_stylesheet
def set_color(self, color):
self.add_style(f"color: {color}")
def set_background_color(self, color):
self.add_style(f"background-color: {color}")
def set_border_radius(self, radius):
self.add_style(f"border-radius: {radius}px")
def set_alignment(self, alignment):
self.add_style(f"text-align: {alignment}")
def add_style(self, new_style):
self.stylesheet.append(new_style)
self.update_stylesheet()
More complex solution: Function decorators.
In this case we would make some assertions about what methods the class has present. Using the decorator, we can call the normal method and have extra logic run afterwards. Using the decorator we can hijack a reference to self and call the update_stylesheet method this way.
def update_method(func):
def wrapper(self, *args, **kwargs):
res = func(self, *args, **kwargs)
self.update_stylesheet()
return res
return wrapper
#update_method
def set_color(self, color):
self.stylesheet.append(f"color: {color}")
#update_method
def set_background_color(self, color):
self.stylesheet.append(f"background-color: {color}")
#update_method
def set_border_radius(self, radius):
self.stylesheet.append(f"border-radius: {radius}px")
#update_method
def set_alignment(self, alignment):
self.stylesheet.append(f"text-align: {alignment}")

Creating instance of a class with random arguments in another class in python

How can an instance of a class that requires an argument be created in another class, with the argument generated randomly?
In the code below I have a 'Square' class that takes a 'height' argument. I then create a 'Cube', based on the 'Square'. The problem is with the 'add_cube' method inside the 'CubeTower' class, where I cannot figure out if I need to provide a 'cube' and 'height' or only one of these arguments. This in turn throws an error when I try to call the 'add_cube' inside a loop that creates cubes with a random side height.
(There are additional methods I have in the 'Cube' class, but they are irrelevant to this problem so I did not include them).
from random import randint
class Square:
def __init__(self, height):
self.height = height
class Cube:
def __init__(self, height):
self.base = Square(height)
class CubeTower:
def __init__(self):
self.tower = []
def add_cube(self, cube, height): # This is where I think I am doing something wrong
cube = Cube(height)
if not self.tower:
self.tower.append(cube)
else:
if cube.base.height < self.tower[-1].base.height:
self.tower.append(cube)
def randomize_tower(self):
for c in range(2, 100):
height = randint(1, 100)
c = Cube(height)
self.add_cube(c, height)
You can do one or the other; a single method doesn't need both.
class CubeTower:
def __init__(self):
self.tower = []
def add_cube(self, cube: Cube):
if not self.tower or cube.base.height < self.tower[-1].base.height:
self.tower.append(cube)
def add_cube_by_height(self, height: int):
self.add_cube(Cube(height))
def randomize_tower(self):
for _ in range(2, 100):
height = randint(1, 100)
# self.add_cube_by_height(height)
c = Cube(height)
self.add_cube(c)
add_cube_by_height is really just a convenience wrapper around add_cube which creates a Cube of a given size for you.

Python method call from class to class

I'm learning Python and I'm getting confused with syntax for calls from one class to another. I did a lot of search, but couldn't make any answer to work. I always get variations like:
TypeError: __init__() takes exactly 3 arguments (1 given)
Help much appreciated
import random
class Position(object):
'''
Initializes a position
'''
def __init__(self, x, y):
self.x = x
self.y = y
def getX(self):
return self.x
def getY(self):
return self.y
class RectangularRoom(object):
'''
Limits for valid positions
'''
def __init__(self, width, height):
self.width = width
self.height = height
def getRandomPosition(self):
'''
Return a random position
inside the limits
'''
rX = random.randrange(0, self.width)
rY = random.randrange(0, self.height)
pos = Position(self, rX, rY)
# how do I instantiate Position with rX, rY?
room = RectangularRoom()
room.getRandomPosition()
You don't need to pass self - that is the newly created instance, and is automatically given by Python.
pos = Position(rX, rY)
Note that the error here is happening on this line, however:
room = RectangularRoom()
The issue on this line is you are not giving width or height.
Maybe the answers to these previous questions help you understand why python decided to add that explicit special first parameter on methods:
What is the purpose of self?
Why do you need explicitly have the "self" argument into a Python method?
Python - why use "self" in a class?
The error message may be a little cryptic but once you have seen it once or twice you know what to check:
Did you forgot to define a self/cls first argument on the method?
Are you passing all the required method arguments? (first one doesn't count)
Those expecting/given numbers help a lot.

Python: confused with classes, attributes and methods in OOP

I'm learning Python OOP now and confused with somethings in the code below.
Questions:
def __init__(self, radius=1):
What does the argument/attribute "radius = 1" mean exactly?
Why isn't it just called "radius"?
The method area() has no argument/attribute "radius".
Where does it get its "radius" from in the code?
How does it know that the radius is 5?
class Circle:
pi = 3.141592
def __init__(self, radius=1):
self.radius = radius
def area(self):
return self.radius * self.radius * Circle.pi
def setRadius(self, radius):
self.radius = radius
def getRadius(self):
return self.radius
c = Circle()
c.setRadius(5)
Also,
In the code below, why is the attribute/argument name missing in the brackets?
Why was is not written like this: def __init__(self, name)
and def getName(self, name)?
class Methods:
def __init__(self):
self.name = 'Methods'
def getName(self):
return self.name
The def method(self, argument=value): syntax defines a keyword argument, with a default value. Using that argument is now optional, if you do not specify it, the default value is used instead. In your example, that means radius is set to 1.
Instances are referred to, within a method, with the self parameter. The name and radius values are stored on self as attributes (self.name = 'Methods' and self.radius = radius) and can later be retrieved by referring to that named attribute (return self.name, return self.radius * self.radius * Circle.pi).
I can heartily recommend you follow the Python tutorial, it'll explain all this and more.
def __init__(self, radius=1):
self.radius = radius
This is default value setting to initialize a variable for the class scope.This is to avoid any garbage output in case some user calls c.Area() right after c = Circle().
In the code below, why is the attribute/argument "name" missing in the brackets?
In the line self.name = 'Methods' you are creating a variable name initialized to string value Methods.
Why was is not written like this: def init(self, name) and def
getName(self, name)?
self.name is defined for the class scope. You can get and set its value anywhere inside the class.
The syntax radius = 1 specifies a parameter "radius" which has a default value of 1:
def my_func(param=1):
... print(param)
...
my_func() #uses the default value
1
my_func(2) #uses the value passed
2
Note that in python there exists more kinds of parameters: positional and keyword parameters, or both.
Usually parameters can be assigned both using the positional notation and the keyword:
>>> def my_func(a,b,c):
... print (a,b,c)
...
>>> my_func(1,2,3)
(1, 2, 3)
>>> my_func(1,2,c=3)
(1, 2, 3)
Python uses "explicit" instance passing, so the first self parameter is used to pass the instance on which the methods are called. You can think of self as being the this of Java. But you must always use it to access instance attributes/methods. You can't call just area(), you must say self.area().
When you do self.attribute = 1 you create a new attribute attribute with value 1 and assign it to the instance self. So in the area() method self.radius refers to the radius attribute of the self instance.
The __init__ method is a special method. It's something similar to a constructor.
It is called when you instantiate the class. Python has a lot of these "special methods", for example the method __add__(self, other) is called when using the operator +.

python's logical error

i've written this simple code, i think that it doesn't contain gramatical error but it doesn't execute so i think that thare is somewhere a logical error :o, so please i need help !! someone can save me ?! :D
class NumberGenerator:
"""A simple class that contains functions to generate ranges of numbers"""
#classmethod
def generate(quantity, value=[]):
while quantity: value.append(quantity) ; quantity -= 1
return value
class Figure:
"""Abstract class for geometric figures"""
def init(self, name):
"""This is the constructor"""
self._name = name
def name(self):
return self._name
class Rectangle(Figure):
"""Rectangle figure"""
def init(self, width, height):
Figure.__init__("rectangle")
self._width = width
self._height = height
def width(self):
return self.width
def height(self):
return self.height
def size(self):
self.width() * self.height()
if __name__ == "__main__":
# We print the range(10,0,-1)
print NumberGenerator.generate(10)
# We print the range(20,0,-1)
print NumberGenerator.generate(20)
# We create a rectangle
r = Rectangle(10, 20)
print r.size()
# EOF
Indentation aside, two errors are right here:
def width(self):
return self.width
def height(self):
return self.height
This should read:
def width(self):
return self._width
def height(self):
return self._height
Also, constructors should be called __init__ and not init.
Finally, the generate() method has several issues:
You probably meant to say #staticmethod and not #classmethod (class methods should take an implicit first argument, which your method doesn't).
The loop is unnecessary (a simple range() call can do the trick).
The method has a subtle bug in it, whereby the second argument (value) is preserved across calls to generate(). If you must extend value, move value = [] into the body of the method.
Your first error message is AttributeError: 'int' object has no attribute 'append' from NumberGenerator.generate due to no self as the first argument and it is then creating the variable quantity as an instance of NumberGenerator.
Once you fix that, your next error is in the fact that init() should actually be __init__(), and once you fix that you have a further error, but I will leave that exercise to yourself.

Categories

Resources