This may be more of a stylistic question, but I am puzzled about classes vs functions in Python.
I have researched this, and I have seen a diversity of opinion:
One school of thought proposes that if you’re not using classes for everything but include statements, you’re a punter-wannabe with no skill.
A less extreme position proposes that classes should be central to your program, but you can have other statements too.
Another school of thought proposes that Python, (and most other languages), have developed a bad, (possibly incurable), case of “Class-itis” and that 99.99999% of the time the same program can be written using functions; and by using functions the code is clearer, cleaner, and easier to maintain.
I am not sure that I am really qualified to discuss this, but - IMHO - I have not seen or done much of anything that would require a class definition and then instances of the class.
Here’s my thinking:
Classes are an abstraction of a concrete programming construct that will vary, have differing characteristics, be used in differing ways, but where all of these things have more things in common than differences - and where the differences represent sub-types of the main type.
In a game, you may have a class called “character” which is an interactive (living) object in the game.
You can have a sub-class, for “pets”.
You can have a sub-class for “monsters” that can be further subdivided by type and characteristics, (hairy apes, tiny pesty-things with stingers or fangs, dragons, ghosts, etc.).
You can have a sub-class for “people” that you interact with in the game, subdivided by type, (human, elf, fairy, dwarf, etc.), friendly or hostile, gender, and so on.
You can have a subclass for “YOU” - the main character - again with it’s own characteristics and attributes.
The bottom line is that all of the various characters in the game have more things in common than they have differences.
Reprogramming every single character as an individual entity would be burdensome and would be a lot of extra work. Having a “global template” that you can modify to create (for example), a class of “monsters” - and then create individual monster types, allows for a consistency of game play and avoids needless errors.
IMHO, classes are useful when you have something that is going to be re-used a lot, in potentially different ways, and would benefit from a “master template” that you can modify to make specific things from. Once that’s done, you can create as many copies of that thing, whenever and wherever you need it. (A cave full of hairy apes, or zillions of tiny pesty-things?)
On the other hand, functions are useful for something that will never vary and will never need major modifications to its functionality.
In my “remote camera robot” project, my 'bot Charlie “shakes his head” at various times to alert me to the fact that he has reached an important step. (i.e. At the end of program startup and at the end of program shutdown, for example.)
I set four variables: hposition, (horizontal position), vposition, (vertical position), hcenter, (the position that represents the “center” position for Charlie’s head horizontally), and vcenter which represents the same thing vertically.
I have three head-positioning functions:
move_head(hposition, vposition)
shake_head( )
center_head( )
move_head moves the head to the coordinates specified.
This is used other places in the software where I command the head to move using the arrow-keys on the keyboard.
center_head moves the head to the coordinates hcenter, vcenter by calling move_head with those coordinates.
This is also called by the “home” key on the keyboard to re-center Charlie’s head on command.
shake_head shakes Charlie’s head both horizontally and vertically by calling:
center_head to give the head a reasonable starting point.
move_head with hcenter + offset
move_head with hcenter - offset
center_head again.
I could have used move_head with hcenter, but I wanted to account for the possibility that there might have been some accidental vertical movement too.
move_head with vcenter + offset
move_head with vcenter - offset
center_head to put Charlie’s head back where it belongs.
The bottom line is that these routines never change, and never change their characteristics. The parameters simply tell the head where to go or what to do.
Since many of the things I do move Charlie’s head, I have broken those functions out into a library called “Head_Motion” that I can use whenever and wherever I want.
However, even though I have created an “include” library for “Head_Motion”, since it never changes, I have not created classes as I have not seen the advantage of creating a class, than creating an instance of the class, just to move Charlie’s head.
Am I understanding this correctly?
Should I be using classes?
Obviously I could create a class for “head motion” and make all these things “methods”, but I don’t see what that buys me in this case. IMHO, if something’s never going to change, and will never need to be overloaded, then a class is pointless.
Caveat - I’m not a “real” programmer. I’ve taken some programming classes, but I mostly just program as a hobby.
I’d say you’re partly right. I learned procedural programming long before I knew about object-oriented programming, and so I’m still more comfortable with that. I do see some programmers who make everything a class (I suspect they learned Java first ; not much choice there). But I still write plenty of procedural code, especially for short scripts.
I wouldn’t go quite that far. You’re making libraries, which I see as another form of encapsulation, even if they’re not true objects. So you’re already well down the path towards the light side (or perhaps you’d see it as the dark side)
When I create a class I almost never need it for creating sub-classes, and I rarely instantiate more than one instance at a time. By using if __name__ == '__main__': I can quickly test changes on the file if I write an appropriate set of tests under the “if” statement, but then still cleanly import the class into other programs. Granted, you could just write a separate test program and import the library, but I find this more self-contained. One of the really useful parts of creating a true object, even if it’s not going to be overloaded, is that you can have a very clean initiation process, and define object level items (e.g. variables, constants) that can be useful across multiple methods in the object. For ROS programming that often means creating additional objects - so having all of those housekeeping chores self-contained is really handy.
So yes - if you want to do multiple levels of inheritance, or need multiple instances of something, then object oriented programming is a must. Even without that there are times when defining a class appropriately can make your code much cleaner and easier to maintain. But I agree there are times when creating objects just doesn’t make a lot of sense, and all you really need is a good procedural program. That’s one of the things I’ve always liked about Python - it can do both reasonably well.
At least that’s my take. Not something I feel dogmatic about - as long as your program runs and does what you want, then you’ve achieved the essential part - the rest is accidental (in the philosophical sense of those terms).
You raise some interesting points which I appreciate.
On the one hand, I’m not sure that the whole “class” paradigm isn’t over done.
Yet you bring up good points about encapsulation, (make that read “avoiding namespace collisions”), that I may wish to investigate further - especially for my “general purpose” libraries for all the stuff I always use, (or may use repeatedly).
I am giving serious thought to making a single class to encapsulate all my head-motion functions turning them into methods of one base class.
My thinking is that even though I may only be using it once, I may we’ll be using it “once” in everything .
I don’t consider using, (or not using), classes either “light” or “dark” - it’s merely a different way of programming that I am not used to.
Machine code doesn’t have classes, and (usually) there is ONE global namespace. That is, unless you are programming on a processor with “ringed” security levels. The stuff I did wasn’t anywhere near that fancy.
Subroutines, (or interrupt processing, which is a special kind of subroutine), are all you get and the responsibility for preventing collisions is 100% on you.
Yep, which is a lot of work. Since I really don’t need microsecond response time (at least not yet) I’m happy to pay the performance overhead to let the VM do the housekeeping for me.
I suspect I’ll need to figure out multi-threaded code at some point, but I’ll cross that bridge when I come to it.
/K
I remember my first absolutely bare-metal project. . .
Up to that point I had been programming assembler on an Atari 8-bit using a well-known macro assembler, (I forget the name).
Any time you wanted to do something, there was a subroutine vector you could call to do anything you wanted.
Disk? Got it!
I/O to the screen? We got’cha covered!
Printing? No problemo, dude!
Etc.
This project had none of that. 16k of ROM, A few k of RAM, and pure hardware interfaces.
I remember beginning to program and hitting a point where I wanted to call a system function and I panicked. . .
“There are NO system functions! What do I do?”
I spun on this for awhile; and then it dawned on me:
I am the GOD of this system and I can do anything I want! I want a print routine? I can make one do whatever I want! Call by value Call by reference? Both Neither? I am GOD and I can make the system do back-flips at my whim.
It was an epiphany!
I finally understood what “bare-metal” was all about and it changed my thinking.
With great power comes great responsibility. Because there is also “I can cause the machine to release the magic smoke.” and “I can let the machine crash hard when some bug that I didn’t realize was there kicks in when the system is used one minute longer than we tried in testing”
Or, it might rain for forty days and forty nights and wipe out all mankind.
Or, it might hit every “marker” you placed in the code to track what it does, but it still won’t erase and program the EEPROM - because you set eeprom_enable to 0x0023 instead of 0x0033 in an included file that you absolutely know that you tested thoroughly!
(I discovered that mistake by - as a last-ditch effort - probing the EEPROM’s pins with a 'scope.)
That’s part of the challenge that comes with the absolute power of bare metal programming.