about
Bits Objects Python
11/26/2023
0
Bits Repo Code Bits Repo Docs

Bits: Python Objects

library and user-defined classes and objects

Synopsis:

This page demonstrates uses of Python User-Defined types and their objects. The purpose is to quickly acquire some familiarity with user-defined types and their implementations.
  • Python defines a few special class methods: parameterized constructors, and other operators for indexing and comparison etc.
  • The compiler does not generate any of these special methods.
  • Also, this is the first set of examples to partition code into several files. That supports readability, may improve translation times, and makes maintenance significantly easier.
Demo Notes  
All of the languages covered in this demonstration support classes. Each class provides a pattern for laying out a memory footprint and defines how data within are accessed. Three of the languages: C++, Rust, and C# provide generics. Each generic function or class is a pattern for defining functions and classes of a specific type. Thus a generic function is a pattern for making type specific functions. A generic class is a pattern for making type specific classes which are patterns for making objects. The other two, Python and JavaScript, are dynamically typed and already support defining functions and classes for multiple types, e.g., no need for generics. This demonstration illustrates use of classes and objects, which for C++, Rust, and C#, are defined in a stackframe or in the heap or both. All data for Python and JavaScript are stored in managed heaps.
The examples below show how to use library and user defined types with emphasis on illustrating syntax and basic operations.

1.0 Source Code

Source code is partitioned into three modules:
  • PointsObj.py defines a custom Point4D class
  • Py_Objects.py demonstrates instances of a few library types, an instance of Point4D, and a small demo of reference behavior.
  • AnalysisObj.py provides functions that help analyze and display operations on these types.

1.1 Point4D

The custom type Point4D has three double precision members representing spatial coordinates and a time member representing the time at which something was at that spatial position.
#------------------------------------------------
# Py_Objects::PointsObj.py
# - User-defined space-time Point class
#------------------------------------------------

import datetime
import time

# point class with three spatial coordinates
class Point4D:
    x = 0.0
    y = 0.0
    z = 0.0
    t = datetime.datetime.now()

    # supports constructor notation
    def __init__(self) -> None:
        pass

    # show named value of Point4D instance
    def show(self, name) :
        print("{} {{".format(name))
        print("  x:{}, y:{}, z:{}".format(self.x, self.y, self.z))
        print("  t:{}".format(self.t))
        print("}")

Concept:

A collection of points might represent the trajectory of a fast ball over home plate, or the track of the eye of a hurricane moving up the east coast.

Implementation:

Python does not directly support private members. The good news is that no accessor or setter methods are needed. The bad news is that program errors may make subtle changes to a point's data, corrupting the computed results. Point4D has only two methods, a constructor, and a show method to display the state of a point.

Code layout:

Python uses block structure determined by whitespace indentation, unlike all the other languages discussed in Bits.

1.2 Source Code Structure

The code fragment below illustrates how this program is partitioned into modules and functions. The source code is available here. Download the entire Bits repository here.
import sys
import copy
import AnalysisObj
import PointsObj

# Python/Py_Objects::Py_Objects.py
#
# Python Dynamic Data Types
#   int, float, complex
#   bytes, bytearray, memoryview
#   list, tuple, range
#   dict, set, frozenset
#   bool
#   str
#   NoneType

# define alias shortcut
anal = AnalysisObj

# # Python requires definition before use ordering

#-- Demonstrate primitive and library types ---------------
def demolibtypes() :
    # code elided

#-- demonstrate user-define type --------------------------
def demouserdeftype() :
    # code elided

# -- illustrate reference behavior ------------------------
def demorefbehavior() :
    # code elided

# -- Demonstration starts here ----------------------------
def execute() :
    print(" Demonstrate Python Objects")
    print("----------------------------")
    print()

    anal.showNote(
        "  All Python types are reference-based\n"\
        "  with values in the managed heap. That\n"\
        "  has consequences we explore in this demo."
    )
    print()

    demolibtypes()
    demouserdeftype()
    demorefbehavior()

    print("\nThat's all folks!\n")

execute()

Structure:

Code in the left panel begins with a set of import statements for the libraries used in this demonstration, two for system resources and two modules defined as separate files in this program.

Execution Flow:

Computation starts with the invocation of function execute(). That in turn calls the demonstration functions demolibtypes(), demouserdeftype(), and demorefbehavior(). This program partitioning into modules and functions makes the code easier to understand and maintain. Statically type languages C++, Rust, and C# all begin processing in function main(). The dynamic languages Python and JavaScript begin processing in a function based on its placement. For Python processing begins in the first function invocation in the program flow. For JavaScript in a web page it is the first function invocation encountered in the HTML body. That may vary depending on events in the window, e.g., button clicks.

1.3 Primitive and Library types

The panels below demonstrate construction and modification of standard Python objects. The statement "d1 = 3.1415927" illustrates that Python does not use type declarations. Types are inferred from use, e.g., d1 is inferred to have the type double. You can use type hints in declarations, but the Python execution engine ignores them. Tools like mypy are available to help catch syntax errors before run-time, but they are not run automatically. The rest of the code illustrates construction and simple modifications of strings and lists. The source code is available here. Download the entire Bits repository here. The two panels below are separated by a "splitter bar". You can drag the bar horizontally to view hidden code or output.
#-- Demonstrate primitive and library types ---------------                                                    
def demolibtypes() :
    anal.showNote("  primitive and library types","\n")

    # type of d1 is inferred as float from RHS
    d1 = 3.1415927
    print("d1 = ", d1)

    # repr(s) wraps string s in quotes
    s1 = "a string"
    print("s1 = ", repr(s1))

    anal.showOp("s2 = s1")
    s2 = s1
    anal.showIdent(s1, "s1")
    anal.showIdent(s2, "s2")

    #print("s2 = {}".format(s2))

    anal.showOp("s2 += \" and more\"")
    s2 += " and more"
    anal.showIdent(s2, "s2")
    anal.showIdent(s1, "s1")
    print()
    anal.showNote(
        "  Assignment, in Python, assigns references not\n"\
        "  values.  So s1 and s2 share same heap instance\n"\
        "  But strings are immutable. So when a change is\n"\
        "  made to one, that creates a new changed instance\n"\
        "  without changing the original."
    )
    print()

    l1 = ["you", "me", "them", "us"]
    anal.showValueEnum(l1, "l1")

    print("\nl1 = ", l1, "\n")

    anal.showOp("l2 = l1")
    l2 = l1

    anal.showOp('l2.append("everyone")')
    l2.append("everyone")
    print("\nl2 = ", l2)
    print("l1 = ", l1, "\n")

    anal.showOp('l2[1] = "myself"')
    l2[1] = "myself"
    print("\nl2 = ", l2)
    print("l1 = ", l1)
    print()

    anal.showNote(
        "  Changes to target of assignment affect source\n"\
        "  except for immutable strings."\
        "  \"caveat emptor\""
    )
    print()

--------------------------------------------------                                                  
  primitive and library types
--------------------------------------------------

d1 =  3.1415927
s1 =  'a string'
--- s2 = s1 ---
s1 a string 1974941848240
s2 a string 1974941848240
--- s2 += " and more" ---
s2 a string and more 1974942127632
s1 a string 1974941848240

--------------------------------------------------
  Assignment, in Python, assigns references not
  values.  So s1 and s2 share same heap instance
  But strings are immutable. So when a change is
  made to one, that creates a new changed instance
  without changing the original.
--------------------------------------------------

l1 {
  you, me, them, us
}

l1 =  ['you', 'me', 'them', 'us']

--- l2 = l1 ---
--- l2.append("everyone") ---

l2 =  ['you', 'me', 'them', 'us', 'everyone']
l1 =  ['you', 'me', 'them', 'us', 'everyone']

--- l2[1] = "myself" ---

l2 =  ['you', 'myself', 'them', 'us', 'everyone']
l1 =  ['you', 'myself', 'them', 'us', 'everyone']

--------------------------------------------------
  Changes to target of assignment affect source
  except for immutable strings.  "caveat emptor"
--------------------------------------------------














                

1.4 User-defined Type

Code in the panels below illustrate use of the Point4D type defined in section 1.1. Instances are constructed, modified, and displayed. Note especially the demonstration of assignment and deepcopy.
#-- demonstrate user-define type --------------------------                                                                     
def demouserdeftype() :
    anal.showNote("  user defined type","\n")

    anal.showOp("p1a = Point4D()")
    p1a = PointsObj.Point4D()

    anal.showOp("AnalysisObj.showTypeShowable(p1a, \"p1a\", nl)")

    # function defined in AnalysisObj.py
    anal.showTypeShowable(p1a, "p1a", "\n")
    p1a.x = 2
    p1a.y = -3.5
    p1a.z = -42

    print()
    # method defined in Point4D
    p1a.show("p1a")

    anal.showOp("p1b = p1a")
    p1b = p1a  # assignment of reference - no copy of instance.
    p1b.show("p1b")

    anal.showOp("p1b.y = 13")
    p1b.y = 13
    p1b.show("p1b")
    p1a.show("p1a")
    anal.showNote(
        "  Reference assigned, not value.  So change\n"\
        "  in P1b changed source p1a."
    )
    print()

    # copy.deepcopy(p) copies entire object graph of p
    anal.showOp("p1c = copy.deepcopy(p1b)")
    p1c = copy.deepcopy(p1b)
    p1c.show("p1c");
    p1b.show("p1b")

    anal.showOp("p1c.z = 12")
    p1c.z = 12
    p1c.show("p1c")
    p1b.show("p1b")
    anal.showNote(
        "  p1c.z reference assigned, not value. But no\n"\
        "  change in p1b since p1c is deep clone of p1b."
    )
    print()









--------------------------------------------------                                                                             
  user defined type
--------------------------------------------------

--- p1a = Point4D() ---
--- AnalysisObj.showTypeShowable(p1a, "p1a", nl) ---
p1a <class 'PointsObj.Point4D'> dynamic
p1a {
  x:0.0, y:0.0, z:0.0
  t:2024-01-14 18:41:57.225653
}

p1a {
  x:2, y:-3.5, z:-42
  t:2024-01-14 18:41:57.225653
}
--- p1b = p1a ---
p1b {
  x:2, y:-3.5, z:-42
  t:2024-01-14 18:41:57.225653
}
--- p1b.y = 13 ---
p1b {
  x:2, y:13, z:-42
  t:2024-01-14 18:41:57.225653
}
p1a {
  x:2, y:13, z:-42
  t:2024-01-14 18:41:57.225653
}
--------------------------------------------------
  Reference assigned, not value.  So change
  in P1b changed source p1a.
--------------------------------------------------

--- p1c = copy.deepcopy(p1b) ---
p1c {
  x:2, y:13, z:-42
  t:2024-01-14 18:41:57.225653
}
p1b {
  x:2, y:13, z:-42
  t:2024-01-14 18:41:57.225653
}
--- p1c.z = 12 ---
p1c {
  x:2, y:13, z:12
  t:2024-01-14 18:41:57.225653
}
p1b {
  x:2, y:13, z:-42
  t:2024-01-14 18:41:57.225653
}
--------------------------------------------------
  p1c.z reference assigned, not value. But no
  change in p1b since p1c is deep clone of p1b.
--------------------------------------------------

1.5 Reference Behavior

Assignment and construction from another instance copies handles, not instances. We've see that demonstrated in the preceeding sections. This code looks at a related behavior. A name is bound, possibly temporarily, to some data instance, say d1. If that name is rebound to some other data instance, say d2, that has two possible effects. If there is no other name bound to the original data then that instance is queued for disposal by the Python garbage collector. However, if there is another name bound to d1, then d1 persists and a new instance is created or referenced by the name change statement. This is illustrated in the panel below. The name t5 is bound to the tuple (1, 2, 3). The name t6 is bound to a list containing t5. We then rebind the name t5 to the complex number 1 + j. That rebinding does not affect the object graph of t6. The data (1, 2, 3) is referenced by the list, t6, so it persists even though t5 was rebound to new data.
# -- illustrate reference behavior ------------------------                                                         
def demorefbehavior() :
    # reference behavior - new child object
    anal.showOp("t5 = (1, 2, 3)")
    t5 = (1, 2, 3)
    anal.showIdent(t5, "t5")

    anal.showOp("t6 = [1, t5, \"weird\"]")
    t6 = [1, t5, "weird"]
    anal.showIdent(t6, "t6")
    anal.showType(t6, "t6")
    
    print("-- t5 = 1 + 1j : new object --")
    #new object for t5
    t5 = 1 + 1j
    anal.showIdent(t5, "t5")

    # t6 still refers to old t5 object
    anal.showIdent(t6, "t6")
    print()
    anal.showNote(
        "  new object for t5, t6 not affected", "\n"
    )

    # # reference behavior - iterate over children
    print("-- iterate over t6 children --")
    for i in t6:
        anal.showIdent(i, "elem")

    # uncommenting the two statements below shows all the user
    # and system defined methods

    # print("\n-- iterate over t6 methods --")
    # print(dir(t6))


                  
                  
--- t5 = (1, 2, 3) ---
t5 (1, 2, 3) 1974942114176
--- t6 = [1, t5, "weird"] ---
t6 [1, (1, 2, 3), 'weird'] 1974941790912
t6 <class 'list'> dynamic
value:  [1, (1, 2, 3), 'weird'] , size:  80
-- t5 = 1 + 1j : new object --
t5 (1+1j) 1974941491280
t6 [1, (1, 2, 3), 'weird'] 1974941790912

--------------------------------------------------                                                  
  new object for t5, t6 not affected
--------------------------------------------------

-- iterate over t6 children --
elem 1 1974940467440
elem (1, 2, 3) 1974942114176
elem weird 1974942119920














1.6 Analysis and Display Functions:

This code block contains a set of functions used for analysis and display in the demonstrations shown above. Notice that these functions are a bit simpler than the same functions in C++, Rust, and C#. That simplicity is a consequence of dynamic typing in Python. Types can be used to promote understanding, but are ignored by the Python interpreter. This simplicity is good, but comes at the cost of many errors occurring at run-time instead of compile-time, and those are more difficult to locate and resolve.
#------------------------------------------------   
# Py_Objects::AnalysisObj.py
# - collection of display and analysis functions
#------------------------------------------------

import sys

# Python requires definition before use ordering

# show name, type, value, and size of a Python instance
def showType(t, nm: str, suffix: str = "") :
    print(nm, type(t), "dynamic")
    print("value: ", t, ', size: ', sys.getsizeof(t), suffix)

# generate indent string with n spaces
def indent(i):
    tmpStr = ""
    for i in range(i):
        tmpStr += ' '
    return tmpStr

# fold indexable into rows of width elements indented by
# left spaces
def fold(enum, left, width):
    tmpStr = indent(left)
    for i in range(len(enum)):
        tmpStr += str(enum[i]) + ", "
        if(((i + 1) % width) == 0 and i != 0):
            tmpStr += "\n" + indent(left)
    rIndex = tmpStr.rindex(',')
    tmpStr = tmpStr[:rIndex]
    return tmpStr

# show name, type, value, and size of a Python instance
def showTypeEnum(enum, nm, left = 0, width = 7, suffix = "") :
    # topStr = indent(left) + nm + type(enum) + "dynamic"
    print(indent(left),nm, ' ', type(enum), ' ', "dynamic", sep='')
    print(indent(left), "{", sep='')
    print(fold(enum, left+2, width))
    print(indent(left), "}", sep = '')
    print(indent(left), "size: ", sys.getsizeof(enum), suffix, sep='')

# show value of an enumerable Python instance
def showValueEnum(enum, nm, left = 0, width = 7, suffix = "") :
    # topStr = indent(left) + nm + type(enum) + "dynamic"
    print(indent(left),nm, ' ', sep='', end='')
    print("{", sep='')
    print(fold(enum, left+2, width))
    print(indent(left), "}", sep = '')

# same as showType except uses class method to show value
def showTypeShowable(t, nm, suffix = ""):
    print(nm, type(t), "dynamic")
    t.show(nm)

# show Python id, unique for each instance
def showIdent(t, n, suffix = "") :
    print(n, t, id(t), suffix)

# show emphasized note
def showNote(text, suffix = "", n: int = 50) :
    tmpStr = ""
    for i in range(n):
      tmpStr += '-'
    print(tmpStr)
    print(text)
    print(tmpStr, suffix)

# show delineated string to announce a program operation
def showOp(text):
    print("--- {} ---".format(text))

Functions:

The function showType(t, nm: str, suffix: str = "") displays the type of the data bound to t, a name, nm, which is expected to be a string representation of t, and an optional suffix which is an empty string by default, but may be set to "\n" to emit a line feed at the end. Note that nm and suffix are typed to aid understanding of the function, but those types are ignored by the interpreter. The functions indent and fold are helpers for the showTypeEnum and showValueEnum functions, supporting displays of a long sequence of data as folded rows. Function showTypeShowable uses the Point4D method show to show a representation of instances defined by that class. Note that an instance of any type can be passed to showTypeShowable but if the type is not Point4D that will almost certainly result in a run-time error. The remaining functions are relatively simple and left to the reader to interpret.

2.0 Build

There is no separate build process for Python programs. Passing a program file name to the Python interpreter performs an initial tokenization and partitioning into code blocks each of which are interpreted at run-time.
C:\github\JimFawcett\Bits\Python\Py_Objects
> python Py_Objects.py
 Demonstrate Python Objects
----------------------------
  # remaining output elided

3.0 VS Code

The code for this demo is available in github.com/JimFawcett/Bits. If you click on the Code dropdown you can clone the repository of all code for these demos to your local drive. Then, it is easy to bring up any example, in any of the languages, in VS Code. Here, we do that for Python\Python_Objects. Figure 1. VS Code IDE - Python Objects Figure 3. Debugging Python Objects

4.0 References

Reference Description
Python Tutorial - w3schools Interactive examples
Python Reference - docs.python.org Semi-formal syntax reference