Classes, instances, threads, and other things.

@cyclicalobsessive
@KeithW

One thing that has puzzled me to no end is self/this in classes.

I am reading about classes and such, and I have an idea what might be happening here.

Am I getting this or am I smoking dope?

Summary:
self/this is a pointer, (or reference), to a particular instance of the class.

For Example:
I know that when I open a file, one of the things that happens is I am returned a “file handle” (or whatever your implementation calls it), which is, (AFAIK), a reference to this particular instance of the file being opened, as more than one thing can have a file open, (for read), at the same time.  It is, in essence, a pointer to my “connection” to the file and to where I am within it.

Using That Logic:
“self” in the context of a class, and especially in the constructor of the class, is a reference to your particular instance of the class.

Example Using That Logic:
(P.S.  I don’t know if this is even legal)
Assume that, for whatever ungodly reason, I instantiate a class twice.  (Or, I have two process threads that each instantiate a copy of the class.)

[code]
[more code]
myclass1 = EasyGoPiGo3(use_mutex = True)

[yawn. . . .]
[more mindless code]
myclass2 = EasyGoPiGo3(use_mutex = False)

[and so on]

So, if I am correct, (and I have no way to prove it one way or the other), in the context of myclass1, “self” might be a reference to an object copy located at 0x22ccfee3, whereas “self” in myclass2 might be a reference to an object copy located at 0xffef335, in the same way that two variables, even if they contain the same value, may well be located at two totally different places in memory.[1]

Do I understand “self” correctly?  You set it so that the instance has a pointer to itSELF and knows which instance of which class it is?

i.e.  In myclass1, value_x might equal Plank’s Constant whereas in myclass2 the same value_x might equal Avogadro’s number, (or even the number “7”), and it’s the conjunction of “self” and “value_x” that determines which value you’re talking about.

2 Likes

Yes, but YOU Don’t set it, Python sets it (passes the instance in it). You use it whenever the execution context is inside the class to access the parts of the class object.

Everything in Python is an object, and the type of the object tells Python how to treat the object.

If fun() is a function (object), then for val=fun() Python knows to execute the code in the fun function.

If MyClass is a class and myclass1 is an object of type MyClass, with get_speed(self) as its “class function”, and speed as a “class variable”, then for myclass1.get_speed(), Python knows that myclass.get_speed() is actually just a function but the function needs to execute as a class function that has access to the class object that it belongs to.

There may be multiple objects of the same MyClass class, but there is only one MyClass.get_speed(self) method, so Python passes the specific object to the class method so the method knows to operate in the context of the specific object.

myclass1 = MyClass()
myclass2 = MyClass()

Therefore for speed_limit=myclass1.get_speed() is turned into:

“call the MyClass get_speed(self) method/function (the myclass. will be MyClass) and pass it the specific object as a parameter so that it knows the context”:
speed_limit=myclass1.get_speed() is really speed_limit=MyClass.get_speed(myclass1)

This way inside MyClass.get_speed(self), using self is whatever object Python passed in:

class MyClass():
  speed = 200  # initial value but it may change with ```myclass1.speed = 300```

  def get_speed(self):
     return self.speed    # return the speed value for this particular MyClass object

Clearer?

2 Likes

You are right.  I misspoke.

I did not mean to say I “set” (assigned) a value to it.  I wanted to say “self” is set, (internally), to establish what instance you’re talking to.

So, if I understand correctly, "self" identifies a unique instance of the class, (likely by pointing to a class instance data structure), since more than one instance of a class may exist at the same time, and they may be doing different things.  “Self” exists inside the class definition so that an instance of the class can know which instance it is.

Theoretically, that implies that you can set the instance identifier to something else, accessing a different instance of the class, though that would be major-league hinkey, would be bizarre in multiple ways, and would likely get you a massive crash - though conceptually, it would be interesting!

Hopefully no malware writers are reading this. . . .

But then again, if you’re trying to get a pub-sub model of message push to a browser to work, the class may not be so much “fun()” after all.  :wink:
 

P.S.
I wonder what happens if you print(self) inside a class. . . .

I’m a BAAAAD BOY - I had to try it.

    def stop(self):
<snip descriptive comments>
        self.set_motor_dps(self.MOTOR_LEFT + self.MOTOR_RIGHT, 0)
        time.sleep(0.1)
        self.set_motor_power(self.MOTOR_LEFT + self.MOTOR_RIGHT, self.MOTOR_FLOAT)
        print(self)  # <== I could not resist!

results in

Each time I start and stop the robot, the object reference is the same.

If I stop the software and re-start it, the instance reference changes as I would expect it to assuming it’s a pointer to an internal data structure.

<easygopigo3.EasyGoPiGo3 object at 0xb560d7d0>

Conclusion:  It IS a pointer!

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

And “theoretically” it appears I am NOT smokin’ dope, as there are other poor sods on Stack Overflow who are trying to do the same thing:

https://stackoverflow.com/questions/47534530/class-instance-as-data-structure-in-python

https://stackoverflow.com/questions/12101958/how-to-keep-track-of-class-instances

And here’s an interesting Wikipedia article on that kind of stuff:
https://en.wikipedia.org/wiki/Object-relational_mapping

2 Likes

I’m nosy, sorry:

I’m curious if you have ever checked Charlie’s throttled flag after using your center routine? I found that if the tilt-pan was sufficiently away from center, commanding both servos to center without allowing one to complete before commanding the other, that the RPi 5v provided by the GoPiGo3 would sag, setting the throttled flag.

def move_head(hpos, vpos):
    servo1.rotate_servo(hpos)
    servo2.rotate_servo(vpos)
    sleep(0.25)
    servo1.disable_servo()
    servo2.disable_servo()
    return(0)

# Center Charlie's head
def center_head():
    move_head(robot['hcenter'], robot['vcenter'])

My fix:

class TiltPan():
...
   def tiltpan_center(self):
      TiltPan.tilt_position = TILT_CENTER
      TiltPan.pan_position = PAN_CENTER
      self.tilt()
      sleep(0.25)  # delay to limit current draw, SG90 spec: 0.12 sec/60deg
      self.pan()
      sleep(0.25)  # delay to ensure action incase next method is off()

    def center(self):
      self.tiltpan_center()

WRT:

There is no “includable” concept in Python. Classes are objects with methods and data for manipulating the object. To create a collected set of “head motions”, the object is the head and the methods are the operations on the object in a class organization.

Ok maybe you mean “to encapsulate” a class object inside another class?

If you want a collection of advanced head motions, you have some choices:

  1. a module of functions and each function takes a head object or robot object parameter
  2. a derived-class of the head class, perhaps called SmartHead, which adds all the advanced motion methods (remembering to init the super (Head class) in the derived-class init.
  3. convenience methods in an encapsulating class:
  • Charlie
    • Head (Charlie().head=Head())

You can also create your Charlie class, which has naming methods for encapsulated class objects like the Head:

class Head():
   ...
   def __init__(self, egpg):   # Pass the EasyGoPiGo3 object to the Head class initialization 
    ...

    def center(self):   

class Charlie(easygopigo3.EasyGoPigo3):

    head = None
    
    def __init__(self,...):
        ...
        self.head = Head(self)    # pass the EasyGoPiGo3 object to the Head class to init both servos
                                  # passes (egpg=self) to Head.__init__(...)
        # optional center when creating the Head object
        self.head.center()  # or could use convenience func defined below: self.center()

    def center(self):   # convenience function of the Charlie object that manipulates Charlie's head
        self.head.center()



Usage:

charlie = Charlie()
charlie.center()  # use Charlie.center() convenience method
or
charlie.head.center()  # use Head.center() method

note: charlie.center() is shorter but doesn't make as much "sense" as charlie.head.center()
but demonstrates the concept of a convenience method.

likewise you could put a bunch of functions in a module and pass Charlie or pass Charlie’s head:

headmotions.py:

import robot.py
import head.py

def nod_yes(robot):
    robot.head.up()
    robot.head.down()
    robot.head.center()

again you can put a convenience method in the Charlie class:

import headmotions

class Charlie():
...
   def nod_yes(self):    # convenience function of Charlie class
       headmotions.nod_yes(self)    # passes robot to the nod_yes(robot) function in module headmotions
2 Likes

In my slippery mnumonica, “includable” means something I can “include” in my code using include or from 'x' include 'y'

It’s getting really late, (like 5 am!), so I will look at the rest of this later.

P.S. I messed with my rounding routine, and its getting interesting.  More after I get some sleep.

Spoiler:
Numbers approaching 15 decimal digits of precision get “interesting”, especially if division is used and you’re not using the decimal package.

1 Like

As I said - there is no such concept in Python, BUT there are these really obtuse things called decorators, which I have only used once to wrap every program Carl runs with a write to a run.log when the main() starts and a second write to the run.log when the main() exits:

# runLog.py   Utils for the run log located in /home/pi/Carl/run.log
#
# decorator @runLog.logRun will log a start and end entry:
#          "YYYY-MM-DD HH:MM|<file>.<func>: Started"
#          "YYYY-MM-DD HH:MM|<file>.<func>: Finished"
#
# Usage:
# import runLog
#
# @runLog.logRun
# def main():

@runLog.logRun is a special “include around the following thing” … sort of, but totally obfuscating in my opinion.

“Real Programmers” use decorators in Python, in Java/JavaBeans/JavaScript, and C++ in everything, everyday. At least in Python the decorators tell you where to look for their source code (as long as you use import decorators and not import * from decorators

2 Likes

There’s a song titled Five O’clock World by The Vogues, and I guess it must have been a "Five O’clock World for me too when I wrote that, (at 5 am)

I meant to say “importable”.  My examples should have been a dead givaway. (:wink:)

I have noticed that too with any other situation that requires a little extra “oomph!” from the GoPiGo’s power supply - calculations that stress the CPU, a lot of I/O from a SSD, and so on.

IMHO there’s a design flaw with the GoPiGo where it cannot pass the current required.  I am continuing to research this, but need additional information about the components used on the GoPiGo3 board itself.

1 Like

In the case of this particular class - let’s call it “head motion” - I want it to do these things:

  1. When instantiated, head = head_motion(v_center, h_center)
    where “v_center” and “h_center” are the established “calibration constants” that represent the desired “center” position in the horizontal and vertical directions.

  2. Method “center()” will use those constants to move the head back to it’s “center” position.

  3. Method “move(v_pos, h_pos)” will move the head to the absolute location defined by “v_pos” and “h_pos”.

  4. Method “shake(offset_value)” will move the head back-and-forth in both planes, by adding and subtracting “offset” from “v_center” and “h_center”

Note that one of the implications of this is that “v_center” and “h_center” do NOT have to represent the absolute centered position of the servos, but can represent the desired center position.  (i.e.  If the robot chassis is angled slightly, the “v_position” value may change slightly so that the camera is truly vertical and the scene directly in front of the 'bot is properly centered in the vertical plane even if that means the vertical servo isn’t set to perfectly mechanically centered.)

Secondary Goal:  All methods, except for “move()” would work by passing the appropriate values to “move()” internally.

Stretch Goal:
The “zero offset” values passed at instantiation become calibration offset values such that move(90, 90) becomes move(v_center, h_center) by calculating the offset required to make the absolute x/y values, (referenced to 90/90) be internally referenced relative to v_center/h_center.

I think I will have to define the class something like this:

class head_movement(self, v, h):
    def __init__(self, v, h):
        self.v_center = int(v)  #capture the calibration constants
        self.h_center = int(h)
        self.v_pos = self.v_center  # set the initial values to centered values
        self.h_pos = self.h_center
        self.offset = int(0)  #  cast "zero" as an integer value.
        [somehow or other, incorporate the servo methods, such that
         self.servoN becomes the same as "servoN" in EasyGoPiGo or EasySensors]

    def move(self, v = v_center, h = h_center); # pass default values if not set
        servo1.rotate_servo(hpos)
        servo2.rotate_servo(vpos)
        sleep(0.25)
        servo1.disable_servo()
        servo2.disable_servo()
        return(0)  #  is this necessary?

    def center(self):
        self.move()

    def shake(self, offset = 0)
        servo1.rotate_servo(hcenter + offset)
        sleep(0.25)
        servo1.rotate_servo(hcenter - offset)
        sleep(0.25)
        servo1.rotate_servo(h_center)
        sleep(0.25)
        servo2.rotate_servo(v_center + offset)
        sleep(0.25)
        servo2.rotate_servo(v_center - offset)
        sleep(0.25)
        servo2.rotate_servo(vcenter)
        servo1.disable_servo()
        servo2.disable_servo()
        return(0)

#  end class definition

I am sure I am leaving out 90% of what I need.
Can you give me hints on what I have to do?

I am sure that, somewhere before I define the class, I have to

  • from easysensors import servo
    (or)
  • from easygopigo3 import EasyGoPiGo3
    (and)
  • from time import sleep

My question is, (within the context of defining a class), how do I “instantiate”, (include), the content of the included class(es) so that their methods become part of the defined class without super-classing them?  Do I do something like:

from easygopigo3 import EasyGoPiGo3
foo = EasyGoPiGo3()

servo1 = foo.init_servo('SERVO1')
servo2 = foo.init_servo('SERVO2')

class head_movement(self, v, h):
    def __init__(self, v, h):
        self.v_center = int(v)  #capture the calibration constants
        self.h_center = int(h)
        self.v_pos = self.v_center  # set the initial values to centered values
        self.h_pos = self.h_center
        self.offset = int(0)  #  cast "zero" as an integer value.
        [somehow or other, incorporate the servo methods, such that
         self.servoN becomes the same as "servoN" in EasyGoPiGo or EasySensors]

    def move(self, v = v_center, h = h_center); # pass default values if not set
        servo1.rotate_servo(hpos)
        servo2.rotate_servo(vpos)
        sleep(0.25)
        servo1.disable_servo()
        servo2.disable_servo()
        return(0)  #  is this necessary?
[the rest of the class goes here]

I am also assuming that, were I to include the class inside a program in toto, it “inherits”, (can use), things that were already imported and/or defined prior to the class definition.

1 Like

Study this until the bulb lights:

2 Likes

I think that’s the problem.

Not only is there nobody home, the lights aren’t even on - and I have serious doubts about the electric power too.

Have you seen those “slippery” plastic tubular toys that when you try to grab one of them, it “squirts” out of your hand?  That’s how I feel about classes right now.  Just when I think I have my arms around something, and go try to test it, splurch!, it goes outta my hand and half-way across the room.

FYI: Searching for “python classes” I get “classes where they teach python”, not the python class object.  “Methods” gets me a lot of stuff for defining functions in Python, and precious little about class methods.

Question:
Where did “robot” come from in your example above?

Looking over much of the stuff in your own repo for Carl, I notice very few if any class-based includes.  most everything is a collection of “utility” functions that do things for you.  As far as I recall, I did not see a single importable file that used a class and methods from the ground up.

I may do just that - create modules that are strings of defined procedures, and work with them.

2 Likes

For the most part that is true.

The TiltPan class encapsulates the tilt and pan servos with all associated methods for using the TiltPan.

There are some adventures with extended versions of the EasyGoPiGo3 class because to control initialization it could not inherit from the DI EasyGoPiGo3 class.

And then there is my whole cottage industry around the IMU classes.

As far as the distance sensor helpers, I should have encapsulated all the distance sensor data and methods as a derived class over the DI distance sensor classes, but I was not comfortable with subclassing back then, and never refactored.

And then there is my “Swiss Army Knife EasyPiCamSensor” class that I created around the camera but never integrated into Carl’s daily life.

Perhaps I should have made the battery a class to contain all the characteristics and methods around the battery, but that is after the fact thinking.

Carl doesn’t have any other “Objects” to be candidates for classes.

Could have been charlie.py with class Charlie() to match the example for encapsulation and convenience functions, or could be myeasygopigo3 for your MyEasyGoPiGo3 class. The idea was to show passing the top robot class object to a non-class function defined in a module.

2 Likes

I obviously need to revisit this, as I can see cases already where I want to be able to pass “baseline values” to the module, but cannot seem to do it.

Given:

“Module”:

# a test module with a bunch of includes and stuff. . .
#  these are totally bogus values to verify if they're being handled properly
Value-1 = 1000
Value-2 = -1000

[the rest of the module and its procedures]

“Program”:

import Module
[more stuff]

Module.Value-1 = 33
Module.Value-2 = 27

[. . . . .]

Module.Use_Those_Numbers()
# the result is that of the original values, not the values I set here.

What happened is that I set a “h_center” and “v_center” value in “Module” - which determines the “center” resting position of Charlie’s head - setting them to obviously bogus non-centered values. and then tried to change them to different values in “Program”.  When I tried to center Charlie’s head, I got the original values, not the changed values.

Apparently, though I can “pass” values to “Module” as parameters to Module’s functions, I cannot distinctly “override” variables directly in the module.

So, to make things work, I deliberately made “Module” really stupid and require “Program” to give “Module” explicit parameter instructions.

For the time being, I am going to avoid classes until I have a better grip on that slippery toy, (:wink:), or if/when the electricity comes back on. :man_facepalming:

I haven’t given up, I just don’t want to spin on this and get nothing done.

2 Likes

The problem here is that you are getting confused with context.

This one does work:

module.py:

value1 = 100

def use_value1():
  print("value1:", value1)


program.py:

import module

module.value1 = 200

module.use_value1()

$ python3 program.py 
value1: 200

BUT if you want to change value1 inside a module func, it has to be told to use the global module context:

module.py:

value1 = 100

def use_value1():
  global value1
  print("value1:", value1)
  value1 += 100


program.py:

import module

module.value1 = 200

module.use_value1()

print("module.value1:", module.value1)

$ python3 program.py 
value1: 200
module.value1: 300

2 Likes

Kind-of a different question, but generally on-topic:

Given a program like this:

#!/usr/bin/python3

import sys
from easygopigo3 import EasyGoPiGo3
from time import sleep

mybot = EasyGoPiGo3()

#  Set initial values
accumulated_value = 0
count = 0
num_measurements = 20  #  Number of measurements per test
sleep_time = 0.25  #  How long to wait between individual measurements within a test cycle.
reference_input_voltage = 12.00
five_v_system_voltage = 0.00
measured_battery_voltage = 0.00
measured_voltage_differential = 0.00

# file1 = open("./voltage_test.txt", "a")
file1 = open("./results/voltage_test-"+str(sleep_time)+"sec.txt", "a")

def vround(x, decimal_precision=2):
#  vround, ("Vanilla" rounding), always uses "standard" 5/4 rounding and always rounds away from zero regardless of sign.
#
#  "x" is the value to be rounded using "standard" 4/5 rounding rules
#  always rounding away from zero regardless of sign
#
#  "decimal_precision" is the number of decimal places to round to
#
    if decimal_precision < 0:
        decimal_precision = 0

    exp = 10 ** decimal_precision
    x = exp * x

    if x > 0:
        val = (int(x + 0.5) / exp)
    elif x < 0:
        val = (int(x - 0.5) / exp)
    else:
        val = 0
        
    if decimal_precision <= 0:
        return (int(val))
    else:
        return (val)

def close():
    print("\nThat's All Folks!\n")
    data = [str(count), " measurements were taken at an interval of ", str(sleep_time), " seconds and the average delta-v was ", "{:.3f}".format(vround(accumulated_value/count, 3)), "\n(based on an input reference voltage of ", str(reference_input_voltage), " volts )\n\n"]
    file1.writelines(data)
    print(str(count), " measurements were taken at an interval of", str(sleep_time), "seconds and the average delta-v was ", "{:.3f}".format(vround(accumulated_value/count, 3)), "\n(based on an input reference voltage of ", str(reference_input_voltage), " volts )\n")
    file1.close()
    sys.exit(0)


try:
    while (count < num_measurements):
        measured_battery_voltage =  vround(mybot.get_voltage_battery(), 3)
        five_v_system_voltage = vround(mybot.get_voltage_5v(), 3)
        measured_voltage_differential =  vround((reference_input_voltage - measured_battery_voltage),3)
        accumulated_value += measured_voltage_differential
        count += 1
        print("Measured Battery Voltage =", "{:.3f}".format(measured_battery_voltage))
        print("Measured voltage differential = ", "{:.3f}".format(measured_voltage_differential))
        print("5v system voltage =", "{:.3f}".format(five_v_system_voltage), "\n")
        print("Total number of measurements so far is ", count, "out of ", num_measurements)
        sleep(sleep_time)
    close()

except KeyboardInterrupt:
    print("Keyboard Interrupt")
    close()

Note:
This is the current version of my “battery test” program, and it works so far.

Note the absence of a section named “main”.  This is from the routine that shakes Charlie’s head at startup:

if __name__ == "__main__":
    #Shaking Charlie's head to indicate startup
    print("Shaking Charlie's Head\n")
    center_head()
    sleep(0.25)
    shake_head()
    sleep(0.25)
    print("Ready to go!\n")

    sys.exit(0)

My understanding is that this “main” section, if it exists, is what is executed when the Python script is launched.  Otherwise, (I am assuming based on what I see Python scripts doing), it starts at the first “non-procedure”, (not a class, method or function), part of the script.

Which method is better?

I am assuming “using a ‘main’ section” because it makes procedure flow unambiguous and you can make the rest of the program into a bunch of “modules”, (functions, etc.), that are called by the logic in the “main” part.

What say ye?

Ahhh!

Reading this article:

mentions exactly what is going on.

Apparently __name__ is assigned by the system at file invocation using the following simple rules:

  1. If the file is the one being directly run, then the file’s __name__ is set to __main__, indicating it’s the top-level file.
  2. If the file is being used by another file, (via an import or a class reference, etc.), than __name__ is set to the file’s name.

Rationale:
If you write a module that does something interesting, (like finding even roots of negative numbers :wink:), you can create a “test module” within it.

In other words, you can write a basic “exercise” program that will run if the module is being run “stand-alone” to verify functionality, but will be absolutely ignored if the module is being run as a part of something else.

Additionally, since the if __name__ == "__main__": part is a real, honest-to-Guido “if” statement. you can define what you want to happen if it’s NOT the “main” module:

if __name__ == "__main__":
    do something nice like print out lotto numbers
    or order pizza and beer for delivery.

else:
    do something EVIL
    like performing an "enhanced security erase"
    on your entire hard drive. and then lie about it and say
    "Who, *ME*??"
2 Likes

Ah! I never thought about the else statement here !
That can be nasty :slight_smile:

3 Likes