What's the best way to change, (overload), a pre-existing class, (i.e. a library class, or something like that?)

Greetings!

Given:

  • An existing class with various attributes and characteristics.

    • Example:  the “easygopygo3” class defined within the easygopigo3 library.
       
  • Assume that, for whatever reason, certain characteristics are undesirable or need to be modified for the duration of the project, such that the modification becomes “intrinsic” and sticky.

    • Example:  The default speed is “300”, but you may wish to change it to “500” or “200” or whatever - and have the value stick.  As @cyclicalobsessive discovered, resetting the value is not sufficient as it gets periodically reset to the original value of “300”.  Or, you’d rather have the value of “mutex” equal to “true” instead of “false”.
       
  • (Advanced topic)
    Perhaps a method does something that doesn’t make sense, (speed or the way something turns is implemented THIS way, but you’d rather have it THAT way instead.

    • Example:  You want to prevent the motors from being damaged, or the servo’s you are using need different pulse characteristics, or whatever other reason you might have.

Question:
What is the best way to “overload”, (I believe that’s the correct term), some aspect of a pre-existing class when instantiated, (i.e. gopigo3_robot = EasyGoPiGo3() )?

Thanks!

P.S.
I am assuming this is the correct way to “temporarily” change something - that is within the scope of a particular project or program.

I am also assuming that if you need the change to be more permanent, you would actually modify the code within the library itself, and then rename it to something unique like my_easygopigo3.  (or, perhaps create a new class definition within the library with a different name and your unique changes as an overload to the base class?)

1 Like

This gets interesting …:

  • There are GoPiGo3 red board controller left and right motor speed limits (write-only to ATMEL chip)
  • There is an EasyGoPiGo3 class variable DEFAULT_SPEED (normally 300)
  • There is an EasyGoPiGo3 class variable for saving a (Left=Right) speed limit sent to the GoPiGo3 red board controller (since the controller limits are write-only)
  • There is an EasyGoPiGo3 class variable NO_LIMIT_SPEED (normally 1000)
  • The speed the bot will turn one or both wheels is the smaller of
    • egpg.speed or the red board’s DPS motor limit

ie.

  • default-not-changed=300, (limit=300), egpg.NO_LIMIT_SPEED=1000: egpg.forward() will travel at 300 DPS
  • default changed to 200, egpg.reset_speed() sets limit to 200, egpg.NO_LIMIT_SPEED=1000: egpg.forward() will travel at 200 DPS
  • egpg.set_speed(100) sets limit to 100, egpg.NO_LIMIT_SPEED=1000: egpg.forward() will travel at 100
egpg = EasyGoPiGo3()

# SETUP Default wheel speeds in DPS  
egpg.DEFAULT_SPEED = 200    # This sets the value of the default speed class variable
egpg.reset_speed()          # This sets the speed class variable and motor speed limits to what ever is egpg.DEFAULT_SPEED
                            # This is the same as egpg.set_speed(egpg.DEFAULT_SPEED) or
                            #                     egpg.set_speed()
                            #                     egpg.set_speed(200)

This is only true if you have more than one GoPiGo3 process because each process creates an instance of the EasyGoPiGo3 class and the objects do not share the class variables (no static class variables - all class variables are actually object variables.)

Carl has:

  • Juicer process that sets speed to 150DPS for getting on and off the recharger
  • RPI-Monitor web page which checks the GoPiGo3 battery every 30 seconds or so. (This process invokes the default EasyGoPiGo3() class which would reset the speed to 300 if I didn’t pay attention.)
  • VoiceCommander which instantiates an EasyGoPiGo3 to boss it around
  • healthCheck which instantiates an EasyGoPiGo3() class to watch the battery for safety shutdown
  • wheellog which instantiates an EasyGoPiGo3() class to know when and how much the wheels turn
  • lifelog which instantiates an EasyGoPiGo3() class to record the battery voltage in the life.log at boot time
  • shutdown.sh which instantiates an EasyGoPiGo3() class to log the battery voltage before a requested shutdown

So every process sets speed to 150 and agrees not to change it under penalty of death, but actually I have gotten into the habit of always setting speed before every critical drive or turn command so that VoiceCommander is free to go fast or slow as commanded.

2 Likes

Normally, the process is to derive a new class from the base class and overload (replace) the undesirable methods:

#!/usr/bin/env python3
"""
   Demonstrate Python inheritance and method overload

   Note: A  derived class can call any non-overloaded base functions,
         but overloading totally hides the same named function.

         Also note there is no concept of signature overloading:
         x(y,z) overloads x(a,b,c) and x(a)
"""

class Base():
  def __init__(self):
    self.base_var = "base_var value"

  def base_func(self):
    return "base class base_func return value"

  def fun_func(self):
    return "base class fun_func returning nasty value"

class DerivedFromBase(Base):
  def __init__(self):
    super().__init__()    # initialize the base class
    self.derived_class_var = "derived_class_var value"

  def fun_func(self):
    return "derived class fun_func returns fun value"


base = Base()
derived = DerivedFromBase()

print("base.base_var has value:", base.base_var)
print("base.base_func() returns:",base.base_func())
print("base.fun_func() returns: ",base.fun_func())
print(" ")
print("derived.base_var has value:",derived.base_var)
print("derived.base_func() returns:",derived.base_func())
print("derived.fun_func() returns: ",derived.fun_func())

pi@Carl:~/Carl/Examples/inherit_and_overloading $ python3 test.py 
base.base_var has value: base_var value
base.base_func() returns: base class base_func return value
base.fun_func() returns:  base class fun_func returning nasty value
 
derived.base_var has value: base_var value
derived.base_func() returns: base class base_func return value
derived.fun_func() returns:  derived class fun_func returns fun value

BUT the GoPiGo3 is testing the name of the base class, so this will not work:

class DerivedFromEasyGoPiGo3(EasyGoPiGo3):
  def __init__(self):
    super().__init__()    # initializing the EasyGoPiGo3 will try to init the GoPiGo3() class and fail

  def forward(self):   # my forward that has a timeout and stall detection and obstacle return
    ...

Therefore I put my_easygopigo3.py in /home/pi/Carl/plib and code thus:

import sys
sys.path.insert(1,'/home/pi/Carl/plib')
from my_easygopigo3 import EasyGoPiGo3

# or

import my_easygopigo3 as easygopigo3

NOTE ALSO: the super().__init__() only works in python3. easygopigo3.py uses:

try:
            if sys.version_info[0] < 3:
                super(self.__class__, self).__init__(config_file_path=config_file_path)
            else:
                super().__init__(config_file_path=config_file_path)

to work for both Python2 and Python3. I’m no longer coding anything to be Python2 compatible.

2 Likes

@cyclicalobsessive,

Assume the following:

  1. The easygopigo3 class is implemented with the following characteristics:
    (this is just for example)

==================
class easygopigo3() # exists with the following attributes
ONE = 1
TWO = 2
THREE = 3
BIG_NUM = 6.023x1024

method do_this()
  [a bunch of code]
  self.stop()

[along with other methods and stuff]

[end of class definition]
=============

Now I know that I can re-write the class and rename it - adding, removing, modifying things within it.  However, let’s assume I don’t want to modify the base clas in vitro, I want to make a derived class within the particular code I’m writing that modifies the class like this:

class my_easygopigo3()
   Is derived from easygopigo3 and inherits all it’s characteristics and methods, except that:

ONE = 27
BIG_NUM = abs(planck’s_constant / pi)

method do_this()
  [everything this method already does]
  [adding some additional code at the end]
[end of method]

method do_that()
  everything the original do_this method did
  But with certain changes tacked on to it.
[end of method]

method do_something_else()
  This is a completely new method
  That implements something that never existed before
[end of method]

[. . . . .]
[end of class]
=========

  1. To search for this on-line, what are the “magic words” I should use as search terms.

  2. Can you explain this in simple, block-code notation, giving me a visual framework to understand before you begin writing Hebrew?

  3. Unfortunately, most of the “tutorials” that talk about classes, writing classes, class inheritance, and such like look like this to me:

. . . I stare at it and the only thing I have to show for it is a pounding headache.

Is there somewhere I can learn about this stuff that actually speaks English and not gibberish?  Maybe there’s a Python Classes, Inheritance, and Overloading For Dummies  book somewhere?  :wink:

Thanks!

1 Like

??
Python Class Inheritance
Python method overloading
??

I ran into some problems with sensors or mutexes or something when I tried to inherit from easygopigo3 a long time ago, but I just tried a very basic simple version and it worked, so maybe you will have success:

pi@Carl:~/Carl/Examples/deriveEasyGoPiGo3 $ more my_easygopigo3.py 
#!/usr/bin/env  python3

from easygopigo3 import EasyGoPiGo3



class My_EasyGoPiGo3(EasyGoPiGo3):

  def __init__(self):
    super().__init__()    # Init the EasyGoPiGo3 (and GoPiGo3) base classes
    print("speed from base EasyGoPiGo3() class:", self.speed)
    self.speed = 200   # modified initialization value
    print("speed from My_EasyGoPiGo3() class:", self.speed)

  def forward(self):   #overloaded function
    print("CAUTION!  forward() at {} called".format(self.speed))

def main():
  megpg = My_EasyGoPiGo3()
  print("megpg.volt():",megpg.volt())
  megpg.forward()


if __name__ == "__main__":
    main()




pi@Carl:~/Carl/Examples/deriveEasyGoPiGo3 $ python3 my_easygopigo3.py 
speed from base EasyGoPiGo3() class: 300
speed from My_EasyGoPiGo3() class: 200
megpg.volt(): 10.896
CAUTION!  forward() at 200 called

And to use it in another program:

pi@Carl:~/Carl/Examples/deriveEasyGoPiGo3 $ more use_my_easygopigo3.py 
#!/usr/bin/env python3


from my_easygopigo3 import My_EasyGoPiGo3


megpg = My_EasyGoPiGo3()

print("megpg.volt():",megpg.volt())
megpg.forward()



pi@Carl:~/Carl/Examples/deriveEasyGoPiGo3 $ python3 use_my_easygopigo3.py 
speed from base EasyGoPiGo3() class: 300
speed from My_EasyGoPiGo3() class: 200
megpg.volt(): 10.93
CAUTION!  forward() at 200 called

2 Likes

Given (as you said):

I could do something like:

. . . and when I instantiate my class, I would get everything that the “normal” easygopigo3 class provides, except that the stop() class now has my modification?

IMHO, I can understand if I’m working on development work on the class itself, (or if I need major updates to significantly large portions of the class), I may wish to copy and edit the entire class to create my custom version.

In my case, the lion’s share of the class isn’t being changed and forcing a dependency on a specialized version of the class just to include a couple of relatively small changes seems less than ideal to me.

With something like this, I can use everything “stock” and just include whatever tweaks I want.  Likewise, if there are updates to the base class elsewhere, (like the new methods for dealing with differing motor ticks-per-revolution), they would automatically be incorporated in my derived class.

Am I understanding this correctly?

Update:

I tried this and I had to implement it as follows:

class My_EasyGoPiGo3(EasyGoPiGo3):
    from time import sleep

    def __init__(self):
        super().__init__()    # Init the EasyGoPiGo3 (and GoPiGo3) base classes

    def stop(self):   #overloaded function
        self.set_motor_dps(self.MOTOR_LEFT + self.MOTOR_RIGHT, 0)
        sleep(0.25)
        self.set_motor_power(self.MOTOR_LEFT + self.MOTOR_RIGHT, self.MOTOR_FLOAT)

Apparently, even though the base class imports time, and my program itself imports time, within the context of the overloaded class, I also have to import specific things I use - ergo the from time import sleep.

2 Likes

Yes, butI am haunted by some run-time exception I hit when considering this design for Carl.

Also, you may want to use the more complicated base class init to allow Python 2 or 3

2 Likes

What exception?

How would I do that?

Which is officially depreciated in GoPiGo O/S, AFAIK.

2 Likes

Is Python 2 installed? Everyone defecates on “deprecated” warnings.

Exactly. BTW, If you want to ensure your class is exactly complete, it should have the same signature as the base class EasyGoPiGo3(…) and use the same base class initializer that easygopigo3 is using even though in your case you are initializing an EasyGoPiGo3() where easygopigo3.EasyGoPiGo3() is initializing the GoPiGo3.GoPiGo3() class.

2 Likes

Everyone defecates on speed limits and road-rules, especially here in Russia.  :man_facepalming:

This is already difficult enough, I’ll worry about Python 2, Python 1, and Python 0 when the time comes.  If YOU want to support Python 2, you go right ahead - Python 3 is already enough of a moving target, and it is already sufficiently difficult for me.

In the case of GoPiGo O/S, they don’t offer warnings, they just don’t use code that depends on Python 2 anymore.  For example, they re-designed the control panel to solve the problems depending on Python 2 created.

:wink:

2 Likes

Care to try that again in English?

1 Like

I don’t remember. It might not have been the EasyGoPiGo() class; perhaps it was deriving from the EasyIMU() that wat a problem, so my fears might be misplaced in this case.

2 Likes

I remember you had no end of problems with the IMU classes, mutexes, and a bunch of other stuff about the IMU that I don’t recall just now.

That’s probably it.

1 Like

The “signature” of a class is the name plus the parameters of the __init__ method.

The signature of the EasyGoPiGo3 class is:

EasyGoPiGo3(config_file_path="/home/pi/Dexter/gpg3_config.json", use_mutex=False)

since the initializer is:

def __init__(self, config_file_path="/home/pi/Dexter/gpg3_config.json", use_mutex=False):

The current signature of your My_EasyGoPiGo3() class is:

My_EasyGoPiGo3()

So your class should be (remove my commenting after you understand the differences):

# I usually put all imports at the top of the file - There is PEP-8 for purists.
from time import sleep

class My_EasyGoPiGo3(EasyGoPiGo3):
    # from time import sleep  # PEP-8

    # This signature "hides" functionality of the base class
    # def __init__(self):  
    # so use:  
    def __init__(self, config_file_path="/home/pi/Dexter/gpg3_config.json", use_mutex=False):
        # This init hides functionality of the base class
        # super().__init__()    # Init the EasyGoPiGo3 (and GoPiGo3) base classes
        # This base initializer allows all functionality of the base EasyGoPiGo3 class:
        super().__init__(config_file_path=config_file_path)

    def stop(self):   #overloaded function
        self.set_motor_dps(self.MOTOR_LEFT + self.MOTOR_RIGHT, 0)
        sleep(0.25)
        self.set_motor_power(self.MOTOR_LEFT + self.MOTOR_RIGHT, self.MOTOR_FLOAT)
2 Likes

Makes a lot of sense.

I was copying your code and, having done that it complained bitterly that “time” and “sleep” were undefined until I added that.

Oh, just to keep your PEP’s happy, I put my includes at the top of the file too.
[I was tempted to make a comment about everyone defecating on PEP’s.]

So, taking everything you said into account, my “corrected” overloaded class should be something like this?

class My_EasyGoPiGo3(EasyGoPiGo3):
    def __init__(self, config_file_path="/home/pi/Dexter/gpg3_config.json", use_mutex=False):
# Maybe = True instead?
        super().__init__(config_file_path=config_file_path)

    def stop(self):   #overloaded function
        self.set_motor_dps(self.MOTOR_LEFT + self.MOTOR_RIGHT, 0)
        sleep(0.25)
        self.set_motor_power(self.MOTOR_LEFT + self.MOTOR_RIGHT, self.MOTOR_FLOAT)

In any event, it works. . .

Did you ever try that test on Dave?

1 Like

You said:

So. . . .
If I have to include the “signature” of the class when I reference it in order to let the entirety of the base class “shine through”, why don’t I have to do this?

from easygopigo3 import EasyGoPiGo3([signature])

or this

my_gopigo3 = EasyGoPiGo3([signature])

. . . . in order to fully reference the functionality of the class?

1 Like

. . . and what if I want use_mutex = True?

2 Likes

Change the default in the My_EasyGoPiGo class __init__

that shows me an error in what I told you:

  • the base class init needs to pass use_mutex=use_mutex and the config_file_path=config_file_path
2 Likes

Not when you reference it, when you define it. Then you have the options when you instantiate it.

definition:

class My_EasyGoPiGo3(EasyGoPiGo3):
    MOTOR_FLOAT = -128     # notice this as an error that invalidates my test program!

    def __init__(self, config_file_path="/home/pi/Dexter/gpg3_config.json", use_mutex=True):
        super().__init__(config_file_path=config_file_path, use_mutex=use_mutex)

    def stop(self):   #overloaded function
        self.set_motor_dps(self.MOTOR_LEFT + self.MOTOR_RIGHT, 0)
        sleep(0.25)
        self.set_motor_power(self.MOTOR_LEFT + self.MOTOR_RIGHT, self.MOTOR_FLOAT)

and then instantiation:

megpg_defaults = My_EasyGoPiGo3()  # default json config and use_mutex=True
megpg_no_mutex = My_EasyGoPiGo3(use_mutex=False)
megpg_my_config = My_EasyGoPiGo3(config_file_path='/home/pi/gpg3_config.json')
megpg_my_config_no_mutex =  My_EasyGoPiGo3(config_file_path='/home/pi/charlie_gpg3_config.json', use_mutex=False)

Referencing

print( “Current Battery is {:>5.1f} volts”.format(my_egpg_defaults.volt()+0.8) )

2 Likes

Therefore, can I do this instead of re-writing the base class?

my_gopigo3 = EasyGoPiGo3(use_mutex = True)

Corollary question:
If mutexes are such a great idea, why are they deliberately set “off”?

Or, are they deliberately disconnecting the steering gear on the car to demonstrate to the students, (after they have a hideous crash!), how important accurate steering is?

Second corollary question:
Assuming that my_gopigo3 = EasyGoPiGo3(use_mutex = True) is valid, is there some way of seeing what the instantiated class is actually doing ?  (i.e. Is the instantiated class actually honoring the mutex request.)  Or, does it even know you requested it - as silence doesn’t necessarily mean assent in programming.

============

Update:

I tried it and it didn’t throw an error at runtime, but that doesn’t necessarily mean it worked, or was even recognized. . . .

1 Like