Python [conda env:PY27_Test]

Python Object Classes - Basics

Unless explicitely stated in the code cell, content in this notebook was created and tested in Python 2.7. Cells that were tested under Python 3.x versus Python 2.7 are so noted.

TOC / Named Sections

Inheritance and Multi-Inheritance

The following topics can come in handy when attempting to understand multi-inheritance and complexities that can arise such as "the diamond pattern" and/or unexpected inheritance collisions:


In [1]:
# http://stackoverflow.com/questions/3277367/how-does-pythons-super-work-with-multiple-inheritance
# started with content above and then edited and added to it as needed for this demo
# also made the code Python 3.x compatible

class First(object):
    objCount = 0
    
    def __init__(self):
        print("first")
        self.objCount +=1
        First.objCount +=1
    def greetMe(self, name="Anonymous"):
        print("Hello %s" %name)

class Second(First):
    def __init__(self):
        print("second")
        self.objCount +=1  # note: if we used super() we would not need this line
                           # this line should effect Second and Fourth instances based on 
                           # the inheritance implementation
        Second.objCount +=1
    def doSomething(self):
        print("something stupid")

class Third(First):
    myGVal = 1000
    def __init__(self):
        print("third")
        self.myIVal = 13
        Third.objCount +=1
        
    def changeIVal(self):
        self.myIVal *=10

class Fourth(Second, Third):
    def __init__(self):
        super(Fourth, self).__init__()             # using super() is a best practice for "forward copmatibility"
        print("that's it - Welcome to Fourth!")
        Fourth.objCount +=1
        
class Fifth(Second, Third):
    # this also works ...
    def __init__(self):
        Second.__init__(self)                    # this is not a best practice but is often easier
        print("that's it - Welcome to Fifth!")
        Fifth.objCount +=1

In [2]:
Fourth.__mro__   # Method Resolution Order


Out[2]:
(__main__.Fourth, __main__.Second, __main__.Third, __main__.First, object)

In [3]:
phorth = Fourth()


second
that's it - Welcome to Fourth!

In [4]:
phifth = Fifth()


second
that's it - Welcome to Fifth!

In [5]:
phorth.doSomething()


something stupid

In [6]:
phorth.myGVal


Out[6]:
1000

In [7]:
try:
    phorth.myIVal
except Exception as ee:
    print(type(ee), ee)


(<type 'exceptions.AttributeError'>, AttributeError("'Fourth' object has no attribute 'myIVal'",))

In [8]:
turd = Third()


third

In [9]:
turd.myIVal


Out[9]:
13

In [10]:
turd.changeIVal()
turd.myIVal


Out[10]:
130

In [11]:
try:
    phorth.myIVal
except Exception as ee:
    print(type(ee), ee)


(<type 'exceptions.AttributeError'>, AttributeError("'Fourth' object has no attribute 'myIVal'",))

In [12]:
# not this side effect ... we inherit a method that now changes a global value
# but we do not inherit the global value since it was not called inti __init__
try:
    phorth.changeIVal()
except Exception as ee:
    print(type(ee), ee)


(<type 'exceptions.AttributeError'>, AttributeError("'Fourth' object has no attribute 'myIVal'",))

In [13]:
turd.myGVal


Out[13]:
1000

In [14]:
sekond = Second()


second

In [15]:
sekond.greetMe("Mitch")


Hello Mitch

In [16]:
phurst = First()


first

In [17]:
anotherSekond = Second()
anotherThird = Third()
yetAnotherThird = Third()
anotherFourth = Fourth()


second
third
third
second
that's it - Welcome to Fourth!

In [18]:
# look at the counts ...  Not really what we're after yet ...
print("Object Counts:")
print("-"*72)
print("phurst", phurst.objCount)
print("sekond", sekond.objCount)
print("anotherSekond", anotherSekond.objCount)
print("turd", turd.objCount)
print("phorth", phorth.objCount)
print("phifth", phifth.objCount)
print("-------------------------")
print("First", First.objCount)
print("Second", Second.objCount)
print("Third", Third.objCount)
print("Fourth", Fourth.objCount)
print("Fifth", Fifth.objCount)


Object Counts:
------------------------------------------------------------------------
('phurst', 1)
('sekond', 3)
('anotherSekond', 4)
('turd', 3)
('phorth', 1)
('phifth', 2)
-------------------------
('First', 1)
('Second', 5)
('Third', 3)
('Fourth', 3)
('Fifth', 3)

In [19]:
# Method Resolution Order
print(First.__mro__)
print(Second.__mro__)
print(Third.__mro__)
print(Fourth.__mro__)


(<class '__main__.First'>, <type 'object'>)
(<class '__main__.Second'>, <class '__main__.First'>, <type 'object'>)
(<class '__main__.Third'>, <class '__main__.First'>, <type 'object'>)
(<class '__main__.Fourth'>, <class '__main__.Second'>, <class '__main__.Third'>, <class '__main__.First'>, <type 'object'>)

Counting Objects


In [20]:
# counting objects ...
# what we're really after is the ability to track each child and get a total for all objects in the heirarchy
# let's see if we can clean this up a bit ...

class First(object):
    objCount = 0
    First.objCount = 0
    def __init__(self):
        print("first")
        self.objCount +=1
        First.objCount +=1
    def greetMe(self, name="Anonymous"):
        print("Hello %s" %name)

class Second(First):
    objCount = 0
    Second.objCount = 0
    
    def __init__(self):
        print("second")
        self.objCount +=1  # note: if we used super() we would not need this line
                           # this line should effect Second and Fourth instances based on 
                           # the inheritance implementation
        Second.objCount +=1

    def doSomething(self):
        print("something stupid")

class Third(First):
    myGVal = 1000
    objCount = 0
    Third.objCount = 0
    
    def __init__(self):
        print("third")
        self.myIVal = 13
        self.objCount +=1
        Third.objCount +=1
        
    def changeIVal(self):
        self.myIVal *=10

class Fourth(Second, Third):
    objCount = 0
    Fourth.ojbCount = 0
    
    def __init__(self):
        super(Fourth, self).__init__()
        print("that's it - Welcome to Fourth!")
        Fourth.objCount +=1
        Second.objCount -=1 # correct for accidental increment caused by inheritence
        
class Fifth(Second, Third):
    # this also works ...
    objCount = 0
    Fifth.objCount = 0

    def __init__(self):
        Second.__init__(self)
        print("that's it - Welcome to Fifth!")
        Fifth.objCount +=1
        Second.objCount -=1 # correct for accidental increment caused by inheritence

In [21]:
# make some objects
phirst = First()
sekond = Second()
sekond2 = Second()
turd = Third()
thoid2 = Third()
thoid3 = Third()
thoid4 = Third()
phorth = Fourth()
phorth2 = Fourth()
phifth = Fifth()
Phifth2 = Fifth()
Phifth3 = Fifth()


first
second
second
third
third
third
third
second
that's it - Welcome to Fourth!
second
that's it - Welcome to Fourth!
second
that's it - Welcome to Fifth!
second
that's it - Welcome to Fifth!
second
that's it - Welcome to Fifth!

In [22]:
# Instance object counts
print("phirst: %s" %phirst.objCount)
print("sekond: %s" %sekond.objCount)
print("sekond2: %s" %sekond2.objCount)
print("turd: %s" %turd.objCount)
print("thoid2: %s" %thoid2.objCount)
print("thoid3: %s" %thoid3.objCount)
print("thoid4: %s" %thoid4.objCount)
print("phorth: %s" %phorth.objCount)
print("phorth2: %s" %phorth2.objCount)
print("phifth: %s" %phifth.objCount)
print("Phifth2: %s" %Phifth2.objCount)
print("Phifth3: %s" %Phifth3.objCount)


phirst: 1
sekond: 1
sekond2: 2
turd: 1
thoid2: 2
thoid3: 3
thoid4: 4
phorth: 1
phorth2: 2
phifth: 1
Phifth2: 2
Phifth3: 3

In [23]:
# Counts for the whole class:
print("First(): %s" %First.objCount)
print("Second(): %s" %Second.objCount)
print("Third(): %s" %Third.objCount)
print("Fourth(): %s" %Fourth.objCount)
print("Fifth(): %s" %Fifth.objCount)


First(): 1
Second(): 2
Third(): 4
Fourth(): 2
Fifth(): 3

In [24]:
foist2 = First()
print("phirst: %s" %phirst.objCount)
print("foist2: %s" %foist2.objCount)
print("First(): %s" %First.objCount)


first
phirst: 1
foist2: 2
First(): 2

Over-riding Python Built-in Functions and Operators


In [1]:
# Object for Coordinates of x and y
# uses over-rides to python language methods and operators

# note:  Pythons does not typicall use getter and setter methods
#        Instead, note how we access x and y in next cells ...

class Coordinate(object):
    def __init__(self, x1, y1):
        self.x = x1
        self.y = y1
        
    def __str__(self):
        return "<%s,%s>" %(self.x, self.y) # this format approach prserves original full value
                                           # regardless if it is float or int

    def __eq__(self, otherCoordinate):
        if self.x == otherCoordinate.x and self.y == otherCoordinate.y:
            return True
        else:
            return False
        
    def __add__(self, otherCoordinate):
        return Coordinate(self.x + otherCoordinate.x, self.y + otherCoordinate.y)

In [2]:
coor1 = Coordinate(2, 3)
print("coor1: %s" %coor1)
coor2 = Coordinate(-5, 15)
print("coor2: %s" %coor2)
coor3 = coor1.__add__(coor2)
print("coor3: %s" %coor3)
print(type(coor3)) # Coordinate
print(isinstance(coor3, Coordinate)) # True
print(coor3.x) # -3
print(coor3.y) # 18
print(coor3)   # <-3,18>
print("-"*72)
coor4 = coor1 + coor2
print("coor4: %s" %coor4)   # <-3,18>
print(type(coor4)) # Coordinate
print(isinstance(coor4, Coordinate)) # True
print(coor4.x) # -3
print(coor4.y) # 18
print("coor3 == coor4: %s" %(coor3 == coor4))
print("coor2 == coor4: %s" %(coor2 == coor4))


coor1: <2,3>
coor2: <-5,15>
coor3: <-3,18>
<class '__main__.Coordinate'>
True
-3
18
<-3,18>
------------------------------------------------------------------------
coor4: <-3,18>
<class '__main__.Coordinate'>
True
-3
18
coor3 == coor4: True
coor2 == coor4: False

A Static Class of Functions

Making a Class of Just Methods - What Works

These code examples work as indicated for Python 2.7 or Python 3.x. Note that these differences imply that in Python 2.7 you must instantiate at least one instance of the class to call the class method with. In Python 3.6, tests were successful without needing to do this.


In [1]:
# the object for this example was an answer to a problem first encountered on www.hackerrank.com
# this cell works in Python 2.7 and 3.6
# it sets up a class with an example method and nothing else in it

class Calculator:
    def power(self, n, p):
        if n < 0 or p < 0:
            raise ValueError("n and p should be non-negative")
        else:
            return n**p

In [3]:
# to use this method like a static method call, Python 2.7 still requires
# instantiation of the class method.  Run this cell to see Python 3.6 code errors
# and the working version in Python 2.7

# works in Python 2.7 ... includes 3.6 only code to show the error:
try:
    print(Calculator.power(Calculator, 3, 7))  # 3.6 compatible code
    
except Exception as ee:
    print("Error: %s" %ee)
    print("Using Python 2.7 alternate code:")
    workingCalculator = Calculator()
    print(Calculator.power(workingCalculator, 3, 7))


Error: unbound method power() must be called with Calculator instance as first argument (got classobj instance instead)
Using Python 2.7 alternate code:
2187

In [2]:
# run this same content in Python 3.6 and no error is thrown, the first line just works
# without needing to instantiate the class

class Calculator:
    # repeated from earlier cell so this cell can be re-tested without re-running earlier cells
    def power(self, n, p):
        if n < 0 or p < 0:
            raise ValueError("n and p should be non-negative")
        else:
            return n**p

try:
    print(Calculator.power(Calculator, 3, 7))  # 3.6 compatible code
    
except Exception as ee:
    print("Error: %s" %ee)
    print("Using Python 2.7 alternate code:")
    workingCalculator = Calculator()
    print(Calculator.power(workingCalculator, 3, 7))


2187

Aditional Tests and Research Relating to Static Methods Class

Additional research into related commands and illustrations of the behaviors.


In [4]:
# Under Python 2.7, both type() tests produce identical output that looks like this:
#    <type 'instancemethod'>

class Foo(object):
    # code modified slightly from this post: 
    #   http://stackoverflow.com/questions/37370578/different-way-to-create-an-instance-method-object-in-python
    def method(self):
        print("Foo Method Works!")
f = Foo()
print(type(f.method))
print(type(Foo.method))


<type 'instancemethod'>
<type 'instancemethod'>

In [1]:
# Under Python 3.6, the two type tests now produce diferent output that looks like this:
#     <class 'method'>
#     <class 'function'>

class Foo(object):
    # code modified slightly from this post: 
    #   http://stackoverflow.com/questions/37370578/different-way-to-create-an-instance-method-object-in-python
    def method(self):
        print("Foo Method Works!")
f = Foo()
print(type(f.method))
print(type(Foo.method))


<class 'method'>
<class 'function'>

In [2]:
# Tested in Python 3.6 and Python 2.7 (worked with both)
# Intended to just practice and demo some concepts ... not what we do in real world to get this output
outLst = [0, 0, 0]
outLst[0] = instance_method = f.method
outLst[1] = instance_method.__func__ is Foo.method
outLst[2] = instance_method.__self__ is f

outLst2 = [(str(elem) + "\n") for elem in outLst]  # converts all to string and adds \n to end
for elem in outLst2:
    print(elem[:-1])  # index refereence strips off \n on end since print() will add newline anyway


<bound method Foo.method of <__main__.Foo object at 0x0000000004711D68>>
True
True

In [3]:
# Tested in Python 3.6
# both lines produce:  "Foo Method Works!"
try:
    # try-except added because of tests shown in next cell ...
    Foo.method(Foo)  # requires us to pass in self as the class in order to work in Python 3.6
except Exception as ee:
    print(type(ee))
    print(ee)
f.method()       # just works


Foo Method Works!
Foo Method Works!

In [5]:
# Tested in Python 2.7
''' output from python 2.7 test:

<type 'exceptions.TypeError'>
unbound method method() must be called with Foo instance as first argument (got type instance instead)
Foo Method Works!
'''
try:
    # ran into trouble in Python 2.7 ... so this code captures the error when run in that version
    Foo.method(Foo)  # requires us to pass in self as the class in order to work in PY3.6, but 2.7 still has problems
except Exception as ee:
    print(type(ee))
    print(ee)
f.method()       # just works


<type 'exceptions.TypeError'>
unbound method method() must be called with Foo instance as first argument (got type instance instead)
Foo Method Works!

In [6]:
# Tested in Python 2.7 and 3.6
# making the static call work in Python 2.7 seems to require creating an instance first

Foo.method(f)


Foo Method Works!

In [6]:
# tested in Python 3.6 and Python 2.7 (tests in next cells)

class GetDeclaredName(object):
    '''Get Name of Variable or Function Passed Into Object. Use inside methods to identify what got passed in via args.'''
    def name_of_declaredEement(self, value):
        ''' name_of_declaredElement -->\n\nreturns name of declared element (variable or function) passed into it.'''
        # code first appeared on StackOverflow in this posting:  http://stackoverflow.com/a/1538399/7525365
        for n,v in globals().items():
            if v == value:
                return n
        return None

In [7]:
#Tested in Python 3.6 and Python 2.7
gdn = GetDeclaredName()
lst = [1,2,3]
try:
    print(GetDeclaredName.name_of_declaredEement(GetDeclaredName, lst)) # will throw error in Python 2.7
except Exception as ee:                                                 # works in Python 3.6
    print(type(ee))
    print(ee)


<type 'exceptions.TypeError'>
unbound method name_of_declaredEement() must be called with GetDeclaredName instance as first argument (got type instance instead)

In [8]:
# more tests in both versions of Python

try:
    # function does not like objects or their attributes (variables) - fails in Python 2.7 and 3.6
    print(GetDeclaredName.name_of_declaredEement(GetDeclaredName, coor3)) 
except Exception as ee:
    print(type(ee))
    print(ee)

try:
    # function does not like objects or their attributes (variables) - fails in Python 2.7 and 3.6
    print(GetDeclaredName.name_of_declaredEement(gdn, coor3.x))
except Exception as ee:
    print(type(ee))
    print(ee)

try:
    print(GetDeclaredName.name_of_declaredEement(gdn, lst))  # this works in Python 2.7 and throws error in Python 3.6  
except Exception as ee:
    print(type(ee))
    print(ee)

print("coor3.x exists and has this value: %d" %coor3.x)


<type 'exceptions.TypeError'>
unbound method name_of_declaredEement() must be called with GetDeclaredName instance as first argument (got type instance instead)
<type 'exceptions.AttributeError'>
'int' object has no attribute 'x'
lst
coor3.x exists and has this value: -3

Abstract Object Coding Exmple

This code example comes from a problem provided on www.hackerrank.com (python 30 day challenge):

  • This syntax was created/tested on Python 2.7.
  • For Python 3.6, consult this notebook (which is different): "TMWP_OO_Classes_AbstractClass_PY36.ipynb."

In [95]:
from abc import ABCMeta, abstractmethod

class Book:
    __metaclass__ = ABCMeta                 # sets up abstract class
    def __init__(self,title,author):        # abstract classes can be subclassed
        self.title=title                    # they cannot be instantiated
        self.author=author
        print("Creating Book: " + self.title + " by " + self.author)
        
    @abstractmethod                         # abstract method definition (beginning)
    def display(): pass                     # abstract method definition (continued)
    
class MyBook(Book):                                # if this class adds any abstract methods
    def __init__(self, title, author, price):      # it would be abstract too
        # Book.__init__(self, title, author)       # to be a concrete class that can be instantiated
        super(MyBook, self).__init__(title,author) # it must implement all inherited abstract methods
        self.price = price                         # note: Book. and super() solutions both work           
        print("Creating MyBook: %s" %self.price)   #       but super() is a best practice for forward compatibility

    def display(self):
        print("Title: %s" %self.title)
        print("Author: %s" %self.author)
        print("Price: %d" %self.price)

In [96]:
sciFiAmazing = MyBook("The Lord of the Rings", "J.R.R. Tolkien", 8.5)


Creating Book: The Lord of the Rings by J.R.R. Tolkien
Creating MyBook: 8.5

In [97]:
sciFiAmazing.price


Out[97]:
8.5

In [98]:
sciFiAmazing.display()
sciFiAmazing.title + " by " + sciFiAmazing.author


Title: The Lord of the Rings
Author: J.R.R. Tolkien
Price: 8
Out[98]:
'The Lord of the Rings by J.R.R. Tolkien'

These links are to research and help topics that relate to object code, and in some cases, topics used to create examples in this notebook.


In [ ]: