SoFunction
Updated on 2024-10-30

Python object-oriented programming OOP in-depth analysis [constructors, combinatorial classes, tool classes, etc.].

This article provides an in-depth analysis of Python object-oriented programming OOP. shared for your reference as follows:

Here's an example of OOP with the module file

# File (start)
class Person:
  def __init__(self, name, job=None, pay=0):
     = name
     = job
     = pay
  def last_name(self):
    return ()[-1]
  def give_raise(self, percent):
     = int( * (1+percent))
    print('total percent:%f' % percent)
  def __str__(self):
    return '[Person: %s, %s]' % (, )
class Manager(Person):
  # This is not a very good method overloading approach, in practice we use the following approach
  def give_raise(self, percent, bonus=.1):
     = int( * (1+percent+bonus))
  # This method takes advantage of the fact that class methods can always be called in an instance.
  # In fact regular instance calls are converted to class calls
  # (args...) Automatically converted by Python to (instance,args...)
  # So remember that when calling directly through a class, you must manually pass the instance, in this case the self parameter
  # And you can't write self.give_raise, which would result in a circular call to the
  #
  # So why this form? Because it is significant for the maintenance of future code, as give_raise now
  # Only in one place, i.e. Person's method, and when we need to change it in the future, we only need to change one version of it
  def give_raise(self, percent, bonus=.1):
    Person.give_raise(self, percent+bonus)
if __name__ == '__main__':
  # self-test code
  bob = Person('Bob Smith')
  sue = Person('Sue Jones', job='dev', pay=100000)
  print(bob)
  print(sue)
  print(bob.last_name(), sue.last_name())
  sue.give_raise(.1)
  print(sue)
  print('-'*20)
  tom = Manager('Tom Jones', 'mgr', 50000)
  tom.give_raise(.1)
  print(tom.last_name())
  print(tom)
  print('--All three--')
  for obj in (bob, sue, tom):
    obj.give_raise(.1)
    print(obj)

This example defines the Person class and the Person class has a constructor with default keyword arguments, overrides the __str__ method for print output, defines a method to get last_name, and defines the give_raise method for salary increase. The class Manager inherits from Person, and Manager redefines its give_raise method to get an additional bonus=0.1.

At the end of the code, the self-testing code is writtenif __name__ == '__main__', which is used for testing. The output is as follows:

[Person: Bob Smith, 0]
[Person: Sue Jones, 100000]
Smith Jones
total percent:0.100000
[Person: Sue Jones, 110000]
--------------------
total percent:0.200000
Jones
[Person: Tom Jones, 60000]
--All three--
total percent:0.100000
[Person: Bob Smith, 0]
total percent:0.100000
[Person: Sue Jones, 121000]
total percent:0.200000
[Person: Tom Jones, 72000]

Here it is also possible to add your own unique methods to Manager.

Customized Constructors

The code works fine now, but a closer look reveals that it doesn't seem to make sense to have to provide a mgr working name for the Manager object when we create it: this is already implied by the class itself.

So, to improve on that, we're going to redefine the__init__method, thus providing the mgr string, and, like give_raise's customization, running the original Person in Person through the class name invocation of the__init__

def __init__(self, name, pay):
    Person.__init__(self, name, 'mgr', pay)

Then the instantiation after that becomes:

tom = Manager('Tom Jones', 50000)

OOP is simpler than we think

This is pretty much the all-important concept in Python's OOP mechanism:

  1. Instance Creation - Populating Instance Properties
  2. Behavioral Methods - Encapsulating Logic in Class Methods
  3. Operator overloading - providing behavior for built-in operations like printing
  4. Custom behavior - redefining methods in subclasses to make them special
  5. Custom constructor - adds initialization logic to the superclass step.

Alternative ways of combining classes

Sometimes we can combine classes in other ways. For example, a common coding pattern is to nest objects within each other to form composite objects instead of inheriting them. The following alternative uses__getattr__operator overload method to intercept access to undefined properties. At this point, our code is as follows:

class Person:
  ...same...
class Manager():
  def __init__(self, name, pay):
     = Person(name, 'mgr', pay)
  def give_raise(self, percent, bonus=.1):
    .give_raise(percent+bonus)
  def __getattr__(self, attr):
    return getattr(, attr)
  def __str__(self):
    return str()
if __name__ == '__main__':
  ...same...

In fact, this Manager alternative is a representation of a common code pattern called delegates, a conformant-based structure that manages a wrapped object and passes method calls to it.

Here Manager is not a real Person, so we have to add extra code to assign methods to the embedded object, such as something like__str__Such an operator overloading method must be redefined. So it requires an increased amount of code, and for this example no sensible Python programmer would organize their code in this way, but object embedding, and the design patterns based on it, are still appropriate when embedded objects require more limited interaction with the container than direct custom hiding.

The following code assumes that Department may aggregate other objects in order to treat them as a collection.

class Department:
  def __init__(self, *args):
     = list(args)
  def add_member(self, person):
    (person)
  def give_raise(self, percent):
    for person in :
      person.give_raise(percent)
  def show_all(self):
    for person in :
      print(person)
development = Department(bob,sue)
  development.add_member(tom)
  development.give_raise(.1)
  development.show_all()

The code here uses inheritance and compositing - the Department is a composite that embeds and controls the aggregation of other objects, but the embedded Person and Manager objects themselves use inheritance to customize. As another example, a GUI might similarly use inheritance to customize the behavior or appearance of labels and buttons, but also composite in order to build a larger package of embedded widgets such as input forms, calculators, and text editors.

Use of introspection-type tools

There are a few more minor issues with the Manager class after we customize the constructor as follows:

  1. When it prints, Manager marks him as a Person. it might be more accurate to display the object with the most exact (i.e. lowest level) class.
  2. Secondly, the current display format just shows the data contained in the__str__in the properties without considering future goals. For example, we can't verify that the tom job name has been correctly set to mgr through the Manager's constructor, because we write the__str__did not print this field. Even worse, if we change the field in the__init__assigned to the object, then you must also remember to also update the__str__to display the new name, otherwise it will not be synchronized over time.

We can solve both problems using Python's introspection tools, which are special properties and functions that allow us to access some of the internal mechanisms implemented by the object. For example, there are two hooks in our code that help us solve the problem:

  1. built-ininstance.__class__attribute provides a link from the instance to the class that created it. Classes in turn have a__name__There's another one.__bases__sequences that provide access to superclasses. We use these to print the name of the class of one of the instances created, rather than doing so by hard-coding.
  2. built-inobject.__dict__Properties provide a dictionary with a key/value pair so that each property is attached to a namespace object (including modules, classes, and instances). Because it is a dictionary, we can get a list of keys, index by key, iterate over their values, and so on. We use these to print out each property of any instance, rather than hard-coding it in a custom display.

Here's how these tools are actually used in interactive mode:

>>> from person import Person
>>> bob = Person('Bob Smith')
>>> print(bob)
[Person: Bob Smith, 0]
>>> bob.__class__
<class ''>
>>> bob.__class__.__name__
'Person'
>>> list(bob.__dict__.keys())
['name', 'pay', 'job']
>>> for key in bob.__dict__:
...   print(key,'=>',bob.__dict__[key])
...
name => Bob Smith
pay => 0
job => None
>>> for key in bob.__dict__:
...   print(key,'=>',getattr(bob,key))
...
name => Bob Smith
pay => 0
job => None

A generalized display tool

Open a new file and write the following code: it is a new, standalone module, named, that implements just such a class. Since its__str__The print overload is used for a generic introspection tool that will work for any instance, regardless of the instance's set of properties. And since this is a class, it automatically becomes a common tool: thanks to inheritance, it can be mixed into any class that wants to use its display format. As an added benefit, if we want to change the display of an instance, we only need to modify this class.

# File 
"""Assorted class utilities and tools"""
class AttrDisplay:
  """
  Provides an inheritable print overload method that displays
  instances with their class names and a name-value pair for
  each attribute stored on the instance itself(but not attrs
  inherited from its classes).Can be mixed into any class,
  and will work on any instance.
  """
  def gatherAttrs(self):
    attrs = []
    for key in sorted(self.__dict__):
      ('%s = %s' % (key,getattr(self,key)))
    return ','.join(attrs)
  def __str__(self):
    return '[%s:%s]' % (self.__class__.__name__, ())
if __name__ == '__main__':
  class TopTest(AttrDisplay):
    count = 0
    def __init__(self):
      self.attr1 = 
      self.attr2 =  + 1
       += 2
  class SubTest(TopTest):
    pass
  x, y = TopTest(), SubTest()
  print(x)
  print(y)

Note the document string here, as a general purpose tool we need to add some functionality to generate documents.

Defined here__str__Shows the class of the instance, and all its attribute names and values, sorted by attribute name.

[TopTest:attr1 = 0,attr2 = 1]
[SubTest:attr1 = 2,attr2 = 3]

Naming Considerations for Tool Classes

One final point to consider is that since the AttrDisplayz class in the classtools module is intended to be a general purpose tool for mixing with other arbitrary classes, we must be mindful of potential unintentional naming conflicts with client classes. If a subclass unintentionally defines a gatherAttrs name on its own, it will likely break our class.

To reduce the chance of such name conflicts, Python programmers often add a [single underscore] prefix to methods they don't want to use for anything else, in our case _gatherAttrs. This isn't very reliable; what if a subclass also defines _gatherAttrs? But it's usually good enough, and for methods inside a class, it's a common Python naming convention.

A better but less common approach is to use only the [two underscores] symbol, __gatherAttrs, in front of method names, Python automatically expands such names to include the class name, thus making them truly unique. This feature is often called [pseudo-private class attributes] and will be described later.

The first thing you need to do to use Print as a general purpose tool is to import it from its module, mix it into the top-level class using inheritance, and remove the more specialized__str__method. The new print overloaded method will be inherited by instances of Person, and instances of Manager.

Here is the final form of the class:

# File (start)
from classtools import AttrDisplay
class Person(AttrDisplay):
  """
  Create and process person records
  """
  def __init__(self, name, job=None, pay=0):
     = name
     = job
     = pay
  def last_name(self):
    return ()[-1]
  def give_raise(self, percent):
     = int( * (1+percent))
    print('total percent:%f' % percent)
class Manager(Person):
  """
  A customized Person with special requirements
  """
  def __init__(self, name, pay):
    Person.__init__(self, name, 'mgr', pay)
  def give_raise(self, percent, bonus=.1):
    Person.give_raise(self, percent+bonus)
if __name__ == '__main__':
  # self-test code
  bob = Person('Bob Smith')
  sue = Person('Sue Jones', job='dev', pay=100000)
  print(bob)
  print(sue)
  print(bob.last_name(), sue.last_name())
  sue.give_raise(.1)
  print(sue)
  print('-'*20)
  tom = Manager('Tom Jones', 50000)
  tom.give_raise(.1)
  print(tom.last_name())
  print(tom)

In this version, some new annotations have also been added to document the work done and each of the best practice conventions - a documentation string for functional descriptions and a # for short annotations are used. Running this code now will see all of the object's attributes, and the final problem is solved: since AttrDisplay pulls the class name directly from the self instance, all each object displays the name of its most recent (lowest) class - tom now displays as Manager instead of Person.

[Person:job = None,name = Bob Smith,pay = 0]
[Person:job = dev,name = Sue Jones,pay = 100000]
Smith Jones
total percent:0.100000
[Person:job = dev,name = Sue Jones,pay = 110000]
--------------------
total percent:0.200000
Jones
[Manager:job = mgr,name = Tom Jones,pay = 60000]

This is exactly the kind of more useful display we're after, and our attribute display class has been turned into a [generic tool] that can be mixed into any class via inheritance to take advantage of the display format it defines.

Finally:Storing objects in a database

The objects we create aren't really database records yet; they're just temporary objects in memory, not stored in a more persistent medium like a file. So now it's time to save the objects using a Python feature called [Object Persistence].

Pickle and Shelve

Object persistence is implemented through three standard library modules, all of which are available in Python:

  1. pickle: Serialization between arbitrary Python objects and byte strings
  2. dbm: implements a key-accessible file system to store strings
  3. shelve: uses two other modules to store Python objects in a file in accordance.

Storing objects in the Shelve database

Let's write a new script that stores the objects of the class into the shelve. In a text editor, open a new file named, import the shelve module, open a new shelve with an external filename, assign the objects to the keys in the shelve, and when we're done with it close the shelve because the changes have been made:

# File :store Person objects on a shelve database
from person import Person, Manager
import shelve
bob = Person('Bob Smith')
sue = Person('Sue Jones', job='dev', pay='100000')
tom = Manager('Tom Jones', 50000)
db = ('persondb')
for obj in (bob, sue, tom):
  db[] = obj
()

Note that here we are using the names of the objects as keys and thus assigning them to the shelve, this is done just for convenience, in the shelve the keys can be any string, the only rule is that the keys must be strings and unique. This way, we can store only one object for each key. However, the values we have under the keys can be almost any type of Python object: built-in objects like strings, lists, and dictionaries, user-defined class instances, and nested combinations of all of these.

Running this code with no output means he's probably valid.

Interactive Exploration shelve

At this point, there will be one or more real files in the current directory with names starting with 'persondb'. This is the file we are storing, our database, and is what we need to copy and transfer when we back up or move storage.

These files can be viewed in the interactive command window:

>>> import glob
>>> ('person*')
['', '', '', '', '']
>>> print(open('').read())
'Tom Jones', (1024, 91)
'Sue Jones', (512, 100)
'Bob Smith', (0, 80)
>>> print(open('','rb').read())
b'\x80\x03cperson\nPerson\nq\x00)\x81q\x01}q\x02(X\x03\x00\x00\x00jobq\x03NX\x03\x00\x00\x00payq\x04K\x00X\x04\x00\x00\x00nameq\x05X\t\x00\x00\x00Bob
...more omitted...

This content cannot be deciphered, but we can deal with it using regular Python syntax and development patterns, i.e., opening these files via shelve.

>>> import shelve
>>> db = ('persondb')
>>> for key in db:
...   print(key, '=>', db[key])
...
Bob Smith => [Person:job = None,name = Bob Smith,pay = 0]
Tom Jones => [Manager:job = mgr,name = Tom Jones,pay = 50000]
Sue Jones => [Person:job = dev,name = Sue Jones,pay = 100000]
>>> len(db)
3
>>> bob = db['Bob Smith']
>>> bob.last_name()
'Smith'

Here, in order to load or use the stored objects, we do not necessarily have to import the Person or Manager classes. This is because Python performs a pickle operation on a class instance, which records its self instance attribute, as well as the name of the class the instance was created in and the location of the class.

The result of this approach is that class instances automatically get all their class behavior when imported in the future.

Updating objects in Shelve

Now for the final piece of scripting, write a program that updates an instance each time it runs, as a way of confirming that our objects are really persistent. The following prints out the database and adds one of our stored objects one at a time, tracking its changes:

Bob Smith => [Person:job = None,name = Bob Smith,pay = 0]
Sue Jones => [Person:job = dev,name = Sue Jones,pay = 110000]
Tom Jones => [Manager:job = mgr,name = Tom Jones,pay = 50000]
Tom Jones => [Manager:job = mgr,name = Tom Jones,pay = 50000]
Sue Jones => [Person:job = dev,name = Sue Jones,pay = 121000]
Bob Smith => [Person:job = None,name = Bob Smith,pay = 0]
Bob Smith => [Person:job = None,name = Bob Smith,pay = 0]
Sue Jones => [Person:job = dev,name = Sue Jones,pay = 133100]
Tom Jones => [Manager:job = mgr,name = Tom Jones,pay = 50000]

future direction

With this example, we were able to see all the basic mechanisms of Python's OOP in action, and, learned ways to avoid redundancy and its related maintainability issues in our code, and built fully functional classes to do the actual work. In addition, we created formal database records by storing objects into Python's shelve, thus making their information persistent.

There's more to explore after this, such as expanding the range of tools used to include those that come with Python and those that are freely available in the open source world:

GUI: Add a graphical user interface to browse and update database records. It is possible to build GUIs that can be ported to Python's tkinter, or to third-party tools like WxPython and PyQt. tkinter comes with Python, allows us to build simple GUIs quickly, and is an ideal tool for learning GUI programming skills.

Web Sites: Although GUIs are convenient and fast, the Web wins in terms of ease of use.Web sites can be built with the basic CGI scripting tools that come with Python, or with full-featured third-party web development frameworks.

Databases: If a database becomes larger or more critical, we can move it from shelve to a more full-featured storage mechanism like the open-source ZODB object-oriented database system (OODB), or to a more traditional SQL-based database system like MySQL, Oracle, PostgreSQL, or SQLLite. Python itself comes with a built-in, working SQLLite database.

ORM: If we do migrate to a relational database for storage, we don't necessarily have to sacrifice Python's OOP tools. Object-relational mappers (ORMs) like SQLObject and SQLAlchemy can be used.

Readers interested in more Python related content can check out this site's topic: thePython Object-Oriented Programming Introductory and Advanced Tutorials》、《Python Data Structures and Algorithms Tutorial》、《Summary of Python function usage tips》、《Summary of Python string manipulation techniques》、《Summary of Python coding manipulation techniquesand thePython introductory and advanced classic tutorials

I hope that what I have said in this article will help you in Python programming.