7. Classes

 

 

 

 

Why use classes?

 

 

♦     Implementing new objects

●      Multiple instances

●      Customization via inheritance

●      Operator overloading

 

 

 

 

Class topics

 

♦     Class basics

♦     The class statement

♦     Class methods

♦     Attribute inheritance

♦     Operator overloading

♦     Name spaces and scopes

♦     OOP, inheritance, and composition

♦     Classes and methods are objects

♦     Odds and ends

♦     Class gotchas

 

 

 

 

OOP: The Big Picture

 

 

 

How it works

 

●      All about: “object.attr”

●      Kicks off a search for first “attr” → “inheritance”

●      Searches trees of linked namespace objects

●      Search is DFLR (except in diamonds), per order in class headers

●      Class objects: supers and subs

●      Instance objects: generated from a class

●      Classes can also define expression behavior

 

 

 

 

 

 

 

class C1: …            # make class objects (ovals)

class C2: …

class C3(C1, C2): …    # links to superclasses, search order

 

I1 = C3()              # make instance objects (rectangles)

I2 = C3()              # linked to their class

I1.x                   # finds customized version in C3

 

 

 

 

Why OOP?

 

●      OOP great at code reuse, structure, and encapsulation

●      Program by customizing in new levels of hierarchy, not changing

●      Extra structure of classes virtually required once programs hit 1K lines (see frigcal)

●      Inheritance: basis of specializing, customizing software—lower in tree means customization

●      Example: an employee database app – company, departments, employees

●      Much more complete than functional paradigm (though the two can often work together)

 

 

 

 

A first look: class basics

 

 

 

►   Multiple Instances

►   Specialization by inheritance

►   Implementing operators

 

 

 

 

 

1. Classes generate multiple instance objects

●      Classes implement new objects: state + behavior

●      Calling a class like a function makes a new instance

●      Each instance inherits class attributes, and gets its own

●      Assignments in class statements make class attributes

●      Assignments to “self.attr” make per-instance attributes

 

 

>>> class FirstClass:              # define a class object

...     def setdata(self, value):  # define class methods

...         self.data = value      # self is the instance

...     def display(self):

...         print self.data        # self.data: per instance

 

 

>>> x = FirstClass()             # make two instances

>>> y = FirstClass()             # each is a new namespace

 

>>> x.setdata("King Arthur")     # call methods: self=x/y

>>> y.setdata(3.14159)

 

>>> x.display()                  # self.data differs in each

King Arthur

>>> y.display()

3.14159

 

>>> x.data = "New value"         # can get/set attributes

>>> x.display()                  # outside the class too

New value

 

 

 


 

 

 

 

 

The world’s simplest Python class?

 

 

 

# an empty class, class and instance attrs filled in later

 

>>> class rec: pass         # empty namespace object

 

>>> rec.name = 'Bob'        # just objects with attributes

>>> rec.age  = 40

>>>

>>> print rec.name          # like a struct in C, a record

Bob

 

>>> x = rec()               # instances inherit class names

>>> y = rec()

>>> x.name, y.name

('Bob', 'Bob')

 

>>> x.name = 'Sue'          # but assignment changes x only

>>> rec.name, x.name, y.name

('Bob', 'Sue', 'Bob')

 

 

 

# really just linked dictionaries

 

>>> rec.__dict__.keys()

['age', '__module__', '__doc__', 'name']

>>> x.__dict__.keys()

['name']

>>> y.__dict__.keys()

[]

>>> x.__class__

<class __main__.rec at 0x00BAFF60>

 

 

 

# even methods can be created on the fly (but not typical)

 

>>> def upperName(self):

        return self.name.upper()      # still needs a self

 

>>> rec.method = upperName

>>> x.method()

'SUE'

>>> y.method()        # run method to process y

'BOB'

>>> rec.method(x)     # can call through instance or class

'SUE'

 

 

 

 

 

 

 

2. Classes are specialized by inheritance

 

●      Superclasses listed in parenthesis in class's header

●      Classes inherit attributes from their superclasses

●      Instances inherit attributes from all accessible classes

●      Logic changes made in subclasses, not in-place

 

 

 

 

>>> class SecondClass(FirstClass):        # inherits setdata

...     def display(self):                # changes display

...         print 'Current value = "%s"' % self.data

 

 

>>> z = SecondClass()

>>> z.setdata(42)           # setdata found in FirstClass

 

>>> z.display()             # finds/calls overridden method

Current value = "42"

 

>>> x.display()             # x is a FirstClass instance

New value

 

 

 

 

 

 

 

 

 

 

3. Classes can intercept Python operators

 

●      Methods with names like "__X__" are special hooks

●      Called automatically when Python evaluates operators

●      Classes may override most built-in type operations

●      Allows classes to integrate with Python's object model

 

 

 

 

>>> class ThirdClass(SecondClass):         # isa SecondClass

...     def __init__(self, value):         # "ThirdClass(x)"

...         self.data = value

...     def __add__(self, other):          # "self + other"

...         return ThirdClass(self.data + other)

...     def __mul__(self, other):

...         self.data = self.data * other  # "self * other"

 

 

>>> a = ThirdClass("abc")        # new __init__ called

>>> a.display()                  # inherited method

Current value = "abc"

 

>>> b = a + 'xyz'                # new __add__ called

>>> b.display()

Current value = "abcxyz"

 

>>> a * 3                        # new __mul__ called

>>> a.display()

Current value = "abcabcabc"

 

 

 

 


 

 

 

 

 

 

Demo: A More Realistic Example

Using class instance objects as database records

Key ideas: __init__, methods, operators, subclass, shelves

See Extras\Code\people for code, or work along live

 

 

 

 

 

 

 

 

A closer look: class terminology

 

 

 

→ Dynamic typing and polymorphism are keys to Python

→ “self” and “__init__” are key concepts in Python OOP

 

 

 

♦     Class

●      An object (and statement) which defines inherited members and methods

♦     Instance

●      Objects created from a class, which inherit its attributes; each instance is a new namespace

♦     Member

●      An attribute of a class or instance object, that’s bound to an object

♦     Method

●      An attribute of a class object, that’s bound to a function object (a callable member)

♦     Self

●      By convention, the name given to the implied instance object in methods

♦     Inheritance

●      When an instance or class accesses a class’s attributes

♦     Superclass

●      Class or classes another class inherits attributes from

♦     Subclass

●      Class which inherits attribute names from another class


 

 

 

 

Using the class statement

 

 

♦     Python’s main OOP tool (like C++)

♦     Superclasses are listed in parenthesis

♦     Special protocols, operator overloading: __X__

♦     Multiple inheritance: “class X(A, B, C)”

♦     Search = DFLR, except for new-style BF in diamonds

 

 

General form

 

class <name>(superclass,…):         # assign to name

    data = value                    # shared class data

    def method(self,…):             # methods

        self.member = value         # per-instance data

 

 

Example

 

●      “class” introduces a new local scope

●      Assignments in “class” create class object attributes

●      “self.name = X” creates/changes instance attribute

 

 

class Subclass(Superclass):         # define subclass

    data = 'spam'                   # assign class attr

    def __init__(self, value):      # assign class attr

        self.data = value           # assign instance attr

    def display(self):

        print self.data, Subclass.data     # instance, class

 

>>> x, y = Subclass(1), Subclass(2)

>>> x.display(); y.display()

1 spam

2 spam


 

 

 

 

Using class methods

 

 

♦     “class” statement creates and assigns a class object

♦     Calling a class object generates an instance object

♦     Class methods provide behavior for instance objects

♦     Methods are nested “def” functions, with a ‘self’

♦     ‘self’ is passed the implied instance object

♦     Methods are all ‘public’ and ‘virtual’ in C++ terms

 

 

 

 

 

 

 

Example

 

class NextClass:                    # define class

    def printer(self, text):        # define method

        print text

 

>>> x = NextClass()                 # make instance

>>> x.printer('Hello world!')       # call its method

Hello world!

 

>>> NextClass.printer(x, 'Hello world!')    # class method

Hello world!

 

 

 

Commonly used for calling superclass constructors

 

class Super:

    def __init__(self, x):

        …default code…

 

class Sub(Super):

    def __init__(self, x, y):

        Super.__init__(self, x)        # run superclass init

        …custom code…                  # do my init actions

 

I = Sub(1, 2)

 

 

# See also: super() built-in for generic superclass access

# But this call has major issues in multiple-inheritance trees

# For more details, see LP5E and the last part of this PDF


 

 

 

 

Customization via inheritance

 

 

♦     Inheritance uses attribute definition tree (namespaces)

♦     “object.attr” searches up namespace tree for first “attr”

♦     Lower definitions in the tree override higher ones

 

 

Attribute tree construction:

 

1.    Instance → assignments to ‘self’ attributes

2.    Class → statements (assignments) in class statements

3.    Superclasses → classes listed in parenthesis in header

 

 

 

 

 

 


 

Specializing inherited methods

 

 

♦     Inheritance finds names in subclass before superclass

♦     Subclasses may inherit, replace, extend, or provide

♦     Direct superclass method calls: Class.method(self,…)

 

 

>>> class Super:

...     def method(self):

...         print 'in Super.method'

...

>>> class Sub(Super):

...     def method(self):

...         print 'starting Sub.method'

...         Super.method(self)

...         print 'ending Sub.method'

...

>>> x = Super()

>>> x.method()

in Super.method

>>> x = Sub()

>>> x.method()

starting Sub.method

in Super.method

ending Sub.method

 

 

 

 

 

 

 

 

file: specialize.py

 

class Super:

    def method(self):

        print 'in Super.method'    # default

    def delegate(self):

        self.action()              # expected

 

class Inheritor(Super):

    pass

 

class Replacer(Super):

    def method(self):

        print 'in Replacer.method'

 

class Extender(Super):

    def method(self):

        print 'starting Extender.method'

        Super.method(self)

        print 'ending Extender.method'

 

class Provider(Super):

    def action(self):

        print 'in Provider.action'

 

if __name__ == '__main__':

    for klass in (Inheritor, Replacer, Extender):

        print '\n' + klass.__name__ + '...'

        klass().method()

    print '\nProvider...'

    Provider().delegate()

 

 

% python specialize.py

Inheritor...

in Super.method

 

Replacer...

in Replacer.method

 

Extender...

starting Extender.method

in Super.method

ending Extender.method

 

Provider...

in Provider.action


 

 

 

 

Operator overloading in classes

 

 

 

♦     Lets classes intercept normal Python operations

♦     Can overload all Python expression operators

♦     Can overload object operations: print, call, qualify,...

♦     Makes class instances more like built-in types

♦     Via providing specially-named class methods

 

 

 

class Number:

    def __init__(self, start):              # on Number()

        self.data = start

    def __add__(self, other):               # on x + other

        return Number(self.data + other)

 

 

>>> X = Number(4)

>>> Y = X + 2

>>> Y.data

6

 

 

 

 

 

 

 

 

 

 

Common operator overloading methods

 

 

♦     Special method names have 2 “_” before and after

♦     See Python manuals or reference books for the full set

 

 

Method

Overloads

Called for

__init__

Constructor

object creation: X()

__del__

Destructor

object reclamation

__add__

operator ‘+’

X + Y

__or__

operator ‘|’

X | Y

__repr__

Printing

print X, `X`

__call__

function calls

X()

__getattr__

Qualification

X.undefined

__getitem__

Indexing

X[key], iteration, in

__setitem__

Qualification

X[key] = value

__len__

Length

len(X), truth tests

__cmp__

Comparison, 2.X

X == Y, X < Y

__radd__

operator ‘+’

non-instance + X

__iter__

iteration

for item in X, I=iter(X)

__next__

iteration

next(I)

 

 

 

 

Examples

 

 

 

♦     call: a function interface, with memory

>>> class Callback:

     def __init__(self, color):         # state information

          self.color = color

     def __call__(self, *args):         # support calls

          print 'turn', self.color

 

>>> cb1 = Callback('blue')              # ‘remember’ blue

>>> cb2 = Callback('green')

>>>

>>> Button(command=cb1,…)               # register handler

 

>>> cb1()                               # on events…

turn blue

>>> cb2()

turn green

 

>>> cb3 = (lambda color='red': 'turn ' + color)  # or: defaults

>>> cb3()

'turn red'

 

 

 

♦     getitem intercepts all index references

>>> class indexer:

...     def __getitem__(self, index):

...         return index ** 2

...

>>> X = indexer()

>>> for i in range(5):

...     print X[i],          # __getitem__        

...

0 1 4 9 16

 

 

 

♦     getattr catches undefined attribute references

>>> class empty:

...     def __getattr__(self, attrname):

...        return attrname + ' not supported!'

...

>>> X = empty()

>>> X.age                # __getattr__

'age not supported!'

 

 

 

♦     init   called on instance creation

♦     add intercepts ‘+’ expressions

♦     repr    returns a string when called by ‘print’

 

>>> class adder:

...     def __init__(self, value=0):

...         self.data = value              # init data

...     def __add__(self, other):

...         self.data = self.data + other  # add other

...     def __repr__(self):

...         return `self.data`             # to string

...

>>> X = adder(1)        # __init__

>>> X + 2; X + 2        # __add__

>>> X                   # __repr__

5

 

 

 

♦     iter   called on start of iterations, auto or manual

♦     next called to fetch each item along the way

 

>>> class squares:

...     def __init__(self, start):      # on squares()

...         self.count = start

...     def __iter__(self):             # on iter()

...         return self                 # or other object with state

...     def __next__(self):             # on next()

...         if self.count == 1:

...             raise StopIteration     # end iteration

...         else:

...             self.count -= 1

...             return self.count ** 2

...

>>> for i in squares(5):   # automatic iterations

...     print(i)

...

16

9

4

1

>>> S = squares(10)        # manual iterations

>>> I = iter(S)            # iter() optional if returns self

>>> next(I)

81

>>> next(I)

64

>>> list(I)

[49, 36, 25, 16, 9, 4, 1]

 

 

 

♦     Attribute access management

■    setattr: partial attribute privacy for Python classes

See Extras\Code\Misc\privates.py on class CD

 

■    getattr: full get/set attribute privacy for Python classes

See Extras\Code\OOP\access.py on class CD

 

 

 

 

 

Namespace rules: the whole story

 

 

♦     Unqualified names (“X”) deal with lexical scopes

♦     Qualified names (“O.X”) use object namespaces

♦     Scopes initialize object namespaces: modules, classes

 

 

 

The “Zen” of Python Namespaces

 

 

mod.py

# all 5 Xs are different variables

 

X = 1                           # global

 

def f():

    X = 2                       # local

 

class C:

    X = 3                       # class

    def m(self):
        X = 4                   # local

        self.X = 5              # instance

 

 

 

 

 

Unqualified names: global unless assigned

 

♦     Assignment: “X = value”

●      Makes names local: creates or changes name in the current local scope, unless declared ‘global’

 

♦     Reference: “X”

●      Looks for names in the current local scope, then the current global scope, then the outer built-in scope

 

 

 

Qualified names: object name-spaces

 

♦     Assignment: “X.name = value”

●      Creates or alters the attribute name in the namespace of the object being qualified

 

♦     Reference: “X.name”

●      Searches for the attribute name in the object, and then all accessible classes above it (none for modules)

 


 

Namespace dictionaries

 

 

♦     Object name-spaces: built-in “__dict__” attributes

♦     Qualification == indexing a name-space dictionary

 

●      To get a ‘name’ from a module “M”:

►    M.name

►    M.__dict__['name']

►    sys.modules['M'].name

►    sys.modules['M'].__dict__['name']

►    sys.__dict__['modules']['M'].__dict__['name']

 

 

♦     Attribute inheritance == searching dictionaries

 

>>> class super:

...     def hello(self):

...         self.data = 'spam'      # in self.__dict__

...

>>> class sub(super):

...     def howdy(self): pass

...

>>> X = sub()

>>> X.__dict__               # a new name-space/dict

{}

>>> X.hola = 42              # add member to X object

>>> X.__dict__

{'hola': 42}

 

>>> sub.__dict__

{'__doc__': None, 'howdy': <function howdy at 762100>}

 

>>> super.__dict__

{'hello': <function hello at 769fd0>, '__doc__': None}

 

>>> X.hello()

>>> X.__dict__

{'data': 'spam', 'hola': 42}


 

 

 

 

Optional reading: OOP and inheritance

 

  

♦     Inheritance based on attribute qualification

♦     In OOP terminology: ‘is-a’ relationship

♦     On “X.name”, looks for “name” in:

1.   Instance             ←X’s own name-space

2.   Class                 ←class that X was made from

3.   Superclasses      ←depth-first, left-to-right

 

 

 

Example: a zoo hierarchy in Python

 

 

file: zoo.py

 

class Animal:

    def reply(self):   self.speak()

    def speak(self):   print 'spam'

 

class Mammal(Animal):

    def speak(self):   print 'huh?'

 

class Cat(Mammal):

    def speak(self):   print 'meow'

 

class Dog(Mammal):

    def speak(self):   print 'bark'

 

class Primate(Mammal):

    def speak(self):   print 'Hello world!'

 

class Hacker(Primate): pass

 

 

 

 

 

 

 

 

 

% python

>>> from zoo import Cat, Hacker

>>> spot = Cat()

>>> spot.reply()              # Animal.reply, Cat.speak

meow

>>> data = Hacker()           # Animal.reply, Primate.speak

>>> data.reply()

Hello world!

 

  


 

 

Optional reading: OOP and composition

 

  

♦     Class instances simulate objects in a domain

♦     Nouns→classes, verbs→methods

♦     Class objects embed and activate other objects

♦     In OOP terminology: ‘has-a’ relationship

 

 

Example: the dead-parrot skit in Python

 

file: parrot.py

 

class Actor:

    def line(self): print self.name + ':', `self.says()`

 

class Customer(Actor):

    name = 'customer'

    def says(self): return "that's one ex-bird!"

 

class Clerk(Actor):

    name = 'clerk'

    def says(self): return "no it isn't..."

 

class Parrot(Actor):

    name = 'parrot'

    def says(self): return None

 

class Scene:

    def __init__(self):

        self.clerk    = Clerk()       # embed some instances

        self.customer = Customer()    # Scene is a composite

        self.subject  = Parrot()

 

    def action(self):

        self.customer.line()          # delegate to embedded

        self.clerk.line()

        self.subject.line()

 

 

 

 

 

 

 

 

 

% python

>>> import parrot

>>> parrot.Scene().action()       # activate nested objects

customer: "that's one ex-bird!"

clerk: "no it isn't..."

parrot: None

 

 

 

 

 

 

 

     

  
  

 Classes are objects: factories

 

 

♦     Everything is a first-class ‘object’

♦     Only objects derived from classes are OOP ‘objects’

♦     Classes can be passed around as data objects

 

def factory(aClass, *args):        # varargs tuple

    return apply(aClass, args)     # call aClass

 

class Spam:

    def doit(self, message):

        print message

 

class Person:

    def __init__(self, name, job):

        self.name = name

        self.job  = job

 

object1 = factory(Spam)                      # make a Spam

object2 = factory(Person, "Guido", "guru")   # make a Person

 

  

 

 

 

Methods are objects: bound or unbound

 

 

♦     Unbound class methods: call with a ‘self’

♦     Bound instance methods: instance + method pairs 

 

object1 = Spam()

x = object1.doit        # bound method object

x('hello world')        # instance is implied

 

t = Spam.doit           # unbound method object

t(object1, 'howdy')     # pass in instance


 

 

 

 

Odds and ends

 

 

 

Pseudo-private attributes

 

♦     Data hiding is a convention (until Python1.5 or later)

♦     “We’re all consenting adults” –Python’s BDFL

♦     1.5 name mangling: “self.__X” → “self._Class__X”

♦     Class name prefix makes names unique in “self” instance

♦     Only works in class, and only if at most 1 trailing “_”

♦      Mostly for larger, multi-programmer, OO projects

♦     See __getattr__ above for implementing full privacy

 

 

class C1:

    def meth1(self): self.__X = 88    # now X is mine

    def meth2(self): print self.__X   # becomes _C1__X in I

 

class C2:

    def metha(self): self.__X = 99    # me too

    def methb(self): print self.__X   # becomes _C2__X in I

 

class C3(C1, C2): pass

I = C3()                              # two X names in I

 

I.meth1(); I.metha()

print I.__dict__

I.meth2(); I.methb()

 

% python private.py

{'_C2__X': 99, '_C1__X': 88}

88

99

 

 

 

 

Documentation strings

 

♦     Still not universally used (but very close!)

♦     Woks for classes, modules, functions, methods

●      String constant before any statements

●      Stored in object’s __doc__ attribute

 

 

file: docstr.py

 

"I am: docstr.__doc__"

 

class spam:

    "I am: spam.__doc__ or docstr.spam.__doc__"

 

    def method(self, arg):

        "I am: spam.method.__doc__ or self.method.__doc__"

        code...

 

def func(args):

    "I am: docstr.func.__doc__"

    code...


 

 

 

Classes versus modules

 

 

♦     Modules…

●     Are data/logic packages

●     Creation: files or extensions

●     Usage: imported

 

♦     Classes…

●     Implement new objects

●     Always live in a module

●     Creation: statements

●     Usage: called

 

 

 

 

 

OOP and Python

 

 

♦     Inheritance

●      Based on attribute lookup: “X.name”

♦     Polymorphism

●      In “X.method()”, the meaning of ‘method’ depends on the type (class) of ‘X’

♦     Encapsulation

●      Methods and operators implement behavior; data hiding is a convention (for now)

 

 

class C:

    def meth(self, x):           # like x=1; x=2

        …

    def meth(self, x, y, z):     # the last one wins!

        …

 

class C:

    def meth(self, *args):

        if len(args) == 1:

            …

        elif type(arg[0]) == int:

            …

 

class C:

    def meth(self, x):       # the python way:

        x.operation()        # assume x does the right thing

 

 

 

 

Python’s dynamic nature

 

 

♦      Members may be added/changed outside class methods

>>> class C: pass

...

>>> X = C()

>>> X.name = 'bob'

>>> X.job  = 'psychologist'

 

♦     Scopes may be expanded dynamically: run-time binding

file: delayed.py

def printer():

    print message     # name resolved when referenced

 

% python

>>> import delayed

>>> delayed.message = "Hello"      # set message now

>>> delayed.printer()

Hello


 

 

 

Subclassing builtin types in 2.2 (Advanced)

 

♦     All types now behave like classes: list, str, tuple, dict,…

♦     Subclass to customize builtin object behavior

♦     Alternative to writing “wrapper” code

 

# subclass builtin list type/class

# map 1..N to 0..N-1, call back to built-in version

 

class MyList(list):

    def __getitem__(self, offset):

        print '(indexing %s at %s)' % (self, offset) 

        return list.__getitem__(self, offset - 1)

 

if __name__ == '__main__':

    print list('abc')

    x = MyList('abc')               # __init__ inherited from list

    print x                         # __repr__ inherited from list

    print x[1]                      # MyList.__getitem__

    print x[3]                      # customizes list superclass method

    x.append('spam'); print x       # attributes from list superclass

    x.reverse();      print x

 

% python typesubclass.py

['a', 'b', 'c']

['a', 'b', 'c']

(indexing ['a', 'b', 'c'] at 1)

a

(indexing ['a', 'b', 'c'] at 3)

c

['a', 'b', 'c', 'spam']

['spam', 'c', 'b', 'a']

 

 

 

 

 

“New Style” Classes in 2.2+ (Advanced)

 

 

♦     Adds new features, changes one inheritance case (diamonds)

♦     Py 2.X: only if “object” or a builtin type as a superclass

♦     Py 3.X: all classes automatically new style (“object” added auto)

class newstyle(object):     # NS requires “object” in 2.X only

    …normal code…

 

 

 

♦     Changes: behavior of “diamond” multiple inheritance

Per the linear MRO: the DFLR path, with all but last appearance of each class removed

 

 

>>> class A:      attr = 1             # CLASSIC

>>> class B(A):   pass

>>> class C(A):   attr = 2

>>> class D(B,C): pass                 # tries A before C

>>> x = D()                            # more depth-first

>>> x.attr

1

 

>>> class A(object): attr = 1          # NEW STYLE

>>> class B(A):      pass

>>> class C(A):      attr = 2

>>> class D(B,C):    pass              # tries C before A

>>> x = D()                            # more breadth-first

>>> x.attr

2

 

 

 

♦     Adds: slots, limits legal attributes set

Used to catch typos and limit memory requirements in pathological cases (ONLY!)

 

 

>>> class limiter(object):

...     __slots__ = ['age', 'name', 'job']

...

>>> x.ape = 1000

AttributeError: 'limiter' object has no attribute

 

 

 

 

♦     Adds: properties, computed attributes alternative

Used to route attribute access to databases, approvals, special-case code

 

 

>>> class classic:

...     def __getattr__(self, name):

...         if name == 'age':

...             return 40

...         else:

...             raise AttributeError

... 

>>> x = classic()

>>> x.age                          # <= runs __getattr__

40

 

>>> class newprops(object):

...     def getage(self):

...         return 40

...     age = property(getage, None, None, None)  # get,set,del

...

>>> x = newprops()

>>> x.age                          # <= runs getage

40

 

 

 

♦     Adds: static and class methods, new calling patterns

Used to process class data, instead of per-instance date

 

 

class Spam:                     # static: no self

    numInstances = 0            # class: class, not instance

    def __init__(self):

        Spam.numInstances += 1

    def printNumInstances():

        print "Number of instances:", Spam.numInstances

    printNumInstances = staticmethod(printNumInstances)

 

>>> a = Spam()

>>> b = Spam()

>>> c = Spam()

>>> Spam.printNumInstances()

Number of instances: 3

 

 

 

♦     Function and class decorators (not just newstyle)

Rebinds names to objects that process functions and classes, or later calls to them

 

 

Function name rebinding

 

@funcdecorator

def F():

   ...

 

# is equivalent to…

 

def F():

   ...

F = funcdecorator(F)   # rebind name, possibly to proxy object

 

 

Class name rebinding

 

@classdecorator

class C:

   ...

 

# is equivalent to…

 

class C:

   ...

C = classdecorator(C)   # rebind name, possibly to proxy object

 

 

Use for static methods (and properties, etc.)

 

class C:

   @staticmethod

   def meth():

       ...

 

# is equivalent to…

 

class C:

   def meth():

       ...

   meth = staticmethod(meth)   # rebind name to call handler

 

# ditto for properties

 

class newprops(object):

    @property

    def age(self):     # age = property(age)

        return 40      # use X.age, not X.age()

 

 

Nesting: multiple augmentations

 

@A

@B

@C

def f(): ...

 

# is equivalent to…

 

def f(): ...

f = A(B(C(f)))

 

 

Arguments: closures, retain state for later calls

 

@funchandler(a, b)

def F(…): …

 

# is equivalent to…

 

def F(…): …

F = funchandler(a, b)(F)

 

 

 

 

 

 

For More Details…

 

●      See the complete function decorator and class decorator examples in the top-level Extras\Code\OOP

●      New-style inheritance algorithm, MRO, super(), metaclasses, descriptors, decorator coding, … too much to cover here

●      See the Advanced Topics section, this summary PDF on formal inheritance rules and super(), and the book Learning Python.

 

 

 

 

 

Class gotchas

 

 

 

♦     Multiple inheritance: order matters

●      Solution: use sparingly and/or carefully

 

 

 

file: mi.py

class Super1:

    def method2(self):             # a 'mixin' superclass

        print 'in Super1.method2'

 

class Super2:

    def method1(self):

        self.method2()             # calls my method2??

    def method2(self):

        print 'in Super2.method2'

 

class Sub1(Super1, Super2):

    pass                           # gets Super1's method2

 

class Sub2(Super2, Super1):

    pass                           # gets Super2's method2

 

class Sub3(Super1, Super2):

    method2 = Super2.method2       # pick method manually

 

Sub1().method1()

Sub2().method1()

Sub3().method1()

 

 

 

 

% python mi.py

in Super1.method2

in Super2.method2

in Super2.method2

 


 

 

 

Optional reading: a set class

 

 

♦     Wraps a Python list in each instance

♦     Supports multiple instances

♦     Adds operator overloading

♦     Supports customization by inheritance

♦     Allows any type of component: heterogeneous

 


 

file: set.py

 

class Set:

    def __init__(self, value = []):    # constructor

        self.data = []                 # manages a list

        self.concat(value)

 

    def intersect(self, other):        # other is a sequence

        res = []                       # self is the subject

        for x in self.data:

            if x in other:

                res.append(x)

        return Set(res)                # return a new Set

 

    def union(self, other):

        res = self.data[:]             # copy of my list

        for x in other:

            if not x in res:

                res.append(x)

        return Set(res)

 

    def concat(self, value):           # value: list, Set...

        for x in value:                # removes duplicates

           if not x in self.data:

                self.data.append(x)

 

    def __len__(self):          return len(self.data)

    def __getitem__(self, key): return self.data[key]

    def __and__(self, other):   return self.intersect(other)

    def __or__(self, other):    return self.union(other)

    def __repr__(self):         return 'Set:' + `self.data`

 

 

 

% python

>>> from set import Set

>>> x = Set([1,2,3,4])             # __init__

>>> y = Set([3,4,5])

 

>>> x & y, x | y                   # __and__,__or__,__repr__

(Set:[3, 4], Set:[1, 2, 3, 4, 5])

 

>>> z = Set("hello")               # set of strings

>>> z[0]                           # __getitem__

'h'

>>> z & "mello", z | "mello"

(Set:['e', 'l', 'o'], Set:['h', 'e', 'l', 'o', 'm'])


 

 

 

 

Summary: OOP in Python

 

 

♦     Class objects provide default behavior

●      Classes support multiple copies, attribute inheritance, and operator overloading

●      The class statement creates a class object and assigns it to a name

●      Assignments inside class statements create class attributes, which export object state and behavior

●      Class methods are nested defs, with special first arguments to receive the instance

 

♦     Instance objects are generated from classes

●      Calling a class object like a function makes a new instance object

●      Each instance object inherits class attributes, and gets its own attribute namespace

●      Assignments to the first argument ("self") in methods create per-instance attributes

 

♦     Inheritance supports specialization

●      Inheritance happens at attribute qualification time: on “object.attribute”, if object is a class or instance

●      Classes inherit attributes from all classes listed in their class statement header line (superclasses)

●      Instances inherit attributes from the class they are generated from, plus all its superclasses

●      Inheritance searches the instance, then its class, then all accessible superclasses (depth-first, left-to-right)

 

 

 

 

Lab Session 6

 

Click here to go to lab exercises

Click here to go to exercise solutions

Click here to go to solution source files

Click here to go to lecture example files