Classes

7 minute read

Published:

This lesson covers An Informal Introduction to Python 3.10.5, https://docs.python.org/3/tutorial/introduction.html

Introduction

  • bundling data and functionality together

  • Creating new class creates new type of object - allow to create new instances

  • Each class instance can have:

    • attributes attached to its main class
    • methods, to modify its state, defined by the class
  • Python

    • class mechanism adds classes with a minimum of new syntax and semantics
    • provide all the standard features of Object Oriented Programming
      • class inheritance mechanism allows multiple base classes
      • derived class can override any methods of its base class or classes,
      • method can call the method of a base class with the same name.
    • Objects can contain arbitrary amounts and kinds of data.
    • classes are created at runtime and can be modified further after creation
  • Normally Class members are public

  • Method function is declared with an explicit first argument representing the object, which is provided implicitly by the call.

A Word About Names and Objects

Python Scopes and Namespaces

Scopes and Namespaces Example

def scope_test():
    def do_local():
        spam = "local spam" # no chnage in scope

    def do_nonlocal():
        nonlocal spam # Changed binding to scope_test
        spam = "nonlocal spam"

    def do_global():
        global spam # now module level binding
        spam = "global spam"

    spam = "test spam"
    
    do_local()
    print("After local assignment:", spam) 
    # "test spam"
    
    do_nonlocal()
    print("After nonlocal assignment:", spam) 
    # nonlocal spam
    
    do_global()
    print("After global assignment:", spam) 
    # global spam

scope_test()
print("In global scope:", spam) # global spam

A First Look at Classes

Class Definition Syntax

class ClassName:
    <statement-1>
    .
    .
    .
    <statement-N>

Class Objects

class MyClass:
    """A simple example class"""
    i = 12345

    def f(self):
        return 'hello world'

# Valid Attribute References i and f
print(MyClass.i) # returns integer i
print(MyClass.f) # returns function object

# __doc__ also a valid attribute
print(MyClass.__doc__) # returns doc string

x = MyClass() # instantiation # empty object
x.i = 123
print(x.i) # 123
print(x.f()) # hello world
class MyClass:
    """A simple example class"""
    def __init__(self):
        self.data = []

    def f(self):
        return 'hello world'

# Valid Attribute References i and f
print(MyClass.f) # returns function object

# __doc__ also a valid attribute
print(MyClass.__doc__) # returns doc string

x = MyClass() # initialize data with __init__
print(x.data)
class Complex:
    def __init__(self, realpart, imagpart): 
        self.r = realpart
        self.i = imagpart

x = Complex(3.0, -4.5) # init with arguments
print(x.r, x.i)

Instance Objects

  • There are two kinds of valid attribute names: data attributes and methods.
  • Data attributes need not be declared; like local variables, they spring into existence when they are first assigned to
class MyClass:
    """A simple example class"""
    def __init__(self):
        self.data = []

    def f(self):
        return 'hello world'

x = MyClass()

# Data attributes need not be declared
# like local variables, they spring into existence 
# when they are first assigned to. 

#print(x.counter) # Error
x.counter = 1
print(x.counter)
del x.counter
#print(x.counter) # Error

Method Objects

x.f()

xf = x.f
print(xf())

Class and Instance Variables

class Dog:

    # class variable shared by all instances
    kind = 'canine'  

    def __init__(self, name):
        # instance variable unique to each instance
        self.name = name 

d = Dog('Fido')
e = Dog('Buddy')

print(d.kind) # 'canine'   # shared by all dogs
print(e.kind) # 'canine'   # shared by all dogs
print(d.name) # 'Fido'     # unique to d
print(e.name) # 'Buddy'    # unique to e
# mistaken use of a class variable

class Dog:

    tricks = []       

    def __init__(self, name):
        self.name = name

    def add_trick(self, trick):
        self.tricks.append(trick)

d = Dog('Fido')
e = Dog('Buddy')

d.add_trick('roll over')
e.add_trick('play dead')

# unexpectedly shared by all dogs
print(d.tricks) # ['roll over', 'play dead'] 
# Correct Design

class Dog:

    def __init__(self, name):
        self.tricks = []
        self.name = name

    def add_trick(self, trick):
        self.tricks.append(trick)

d = Dog('Fido')
e = Dog('Buddy')

d.add_trick("roll over")
e.add_trick("play dead")

# unexpectedly shared by all dogs
print(d.tricks) # ['roll over'] 
print(e.tricks) # ['play dead']

Random Remarks

  • If the same attribute name occurs in both an instance and in a class, then attribute lookup prioritizes the instance
class Warehouse:
    purpose = 'storage'
    region = 'west'

w1 = Warehouse()
print(w1.purpose, w1.region) # storage west

w2 = Warehouse()
w2.region = 'east'
print(w2.purpose, w2.region) # storage east

w3 = Warehouse()
w3.price_per_hour = 1
print(w3.purpose, w3.region, w3.price_per_hour) # storage west 1
# Function defined outside the class

def f1(self, x, y):
    return min(x, y)

class C:
    f = f1

    def g(self):
        return 'hello world'

    h = g
    
# f, g, h are all attributes of class C
x = C()

print(x.g()) # hello world
print(x.h()) # hello world
print(x.f(2, 3)) # 2
# Method may call other methods using self

class Bag:
    def __init__(self):
        self.data = []

    def add(self, x):
        self.data.append(x)

    def addtwice(self, x):
        self.add(x)
        self.add(x)

b = Bag()
b.add("C Book")
b.addtwice("Python Book")

print(b.data) # ['C Book', 'Python Book', 'Python Book']

Inheritance

# when baseclass is from same scope
class DerivedClassName(BaseClassName):
    <statement-1>
    ...
    <statement-N>

# when baseclass is from other module
class DerivedClassName(modname.BaseClassName):
    <statement-1>
    ...
    <statement-N>

Multiple Inheritance

class DerivedClassName(Base1, Base2, Base3):
    <statement-1>
    ...
    <statement-N>
  • depth-first, left-to-right
    • DerivedClassName, then Base1, then Base2, then Base3

Private Variables

Odds and Ends

  • Creating structured datatype as in C
# Empty class for C-like structured data type
class Employee:
    pass

john = Employee()  # Create an empty employee record

# Fill the fields of the record
john.name = 'John Doe'
john.dept = 'computer lab'
john.salary = 1000

print(john.name)

Iterators

# most container objects can be looped using for
for element in [1, 2, 3]: # List
    print(element)
    
for element in (1, 2, 3): # Tuple
    print(element)
    
for key in {'one':1, 'two':2}: # Dictionary
    print(key)
    
for char in "123": # String
    print(char)
    
for line in open("data.txt"): # File
    print(line, end='')
s = "abc"
it = iter(s)
print(next(it)) # a
print(next(it)) # b
print(next(it)) # c
#print(next(it)) # StopIteration Exception
s = [1, 2, 3]
it = iter(s)
print(next(it)) # 1
print(next(it)) # 2
print(next(it)) # 3
#print(next(it)) # StopIteration Exception
class Reverse:
    """Iterator for looping a sequence backwards"""
    
    def __init__(self, data):
        self.data = data
        self.index = len(data)
    
    def __iter__(self):
        return self
    
    def __next__(self):
        if self.index == 0:
            raise StopIteration
            
        self.index -= 1
        return self.data[self.index] 
      	#ret char at index position
rev = Reverse("spam") 
# instance of Reverse with data spam index 4

print(rev)

iter(rev) # inplace modifies rev
print(rev) # Iterator

print(next(rev))
print(next(rev))
print(next(rev))
print(next(rev))
#print(next(it))

rev = Reverse("spam")
for char in rev:
    print(char, end=" ")

Generators

  • Generators are a simple and powerful tool for creating iterators.
  • They are written like regular functions but use the yield statement whenever they want to return data.
  • Each time next() is called on it, the generator resumes where it left off (it remembers all the data values and which statement was last executed)
def reverse(data):
    for index in range(len(data)-1, -1, -1):
        return data[index] # return last char and loop terminates
        print("Hi") # Unreachable code 

rev = reverse('golf')
print(rev)
def reverse(data):
    for index in range(len(data)-1, -1, -1):
        yield data[index] # return last char and waits
        #print("Hi", end=" ") # will continue here

rev = reverse('golf')
print(rev) # generator instance due to yield

print(next(rev)) # f

print(next(rev)) # Hi l

print(next(rev)) # Hi o

print(next(rev)) # Hi g

#print(next(rev))

rev = reverse('golf')
for char in rev:
    print(char, end=" ")

Generator Expressions