Introduction

An object is a programming construct which wraps data (in the form of attributes) and functions (in the form of methods).

Object-oriented programming allows programs to be modelled after real-world systems. This approach was originally developed to create simulations, and gained popularity with the rise of graphical user interfaces. It is also an efficient way to reuse code. The macOS operating system and its GUI framework, Cocoa, are written in an object-oriented programming language, Objective-C. RoboFont itself is written in PyObjC, a Python bridge to Objective-C.

Objects are also central to vanilla, FontParts, and most other libraries used by RoboFont and mentioned in this documentation.

Objects in Python

Even though Python is often referred to as an object-oriented language, it does not impose objects: it is perfectly valid to write Python code without using classes, object inheritance, or any other mechanism associated with object-oriented programming.

In Python, everything is an object: functions, classes, strings, exceptions… these are all standard types which are built into the interpreter. Like any object, they have a type, they can be passed as function arguments, and they may have methods and properties. For example, string objects have methods like .split() and .strip(), integers can be added, lists can be indexed, etc.

In addition to providing several standard types, Python also allows us to create our own object types using classes. A class is an abstract definition of an object, like a blueprint. Individual objects are created by instantianting a class.

Classes and instances

As with functions, working with classes involves two steps:

  1. defining the class
  2. creating one or more instances (objects) from the class

Classes are created using the class definition. Instances are created by ‘calling’ the class, using the same parentheses notation as functions:

# 1. define class
class Dog:
    pass

# 2. create instances
D1 = Dog()
D2 = Dog()

Object attributes and methods

Objects can have attributes and methods, which can be accessed by reference using the dot operator notation: object.attribute. Object methods can be called using parentheses, like normal functions: object.method().

class Dog:

    # attribute
    legs = 4

    # method
    def walk(self):
        print('the dog is walking...')

D = Dog()

# get attribute value
print(f'the dog has {D.legs} legs.')

# call method
D.walk()
the dog has 4 legs.
the dog is walking...

Notice that, when defined, class methods take the object itself as their first argument; it is a convention in Python to call this variable self. When an instance method is called, the instance object is passed to the method implicitly.

The class constructor method

Classes may include a special method named __init__, which is called when an instance is created:

class Dog:

    def __init__(self):
        print('a dog is born...')

D = Dog()
a dog is born...

The __init__ method may take more arguments, which must be passed to the class when it is instantiated:

class Dog:

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

D1 = Dog('Rex')
D2 = Dog('Charlie')

print(D1.name)
print(D2.name)
Rex
Charlie

Class attributes vs. instance attributes

Objects may have class attributes, which are shared by all instances, and instance attributes, which are specific to individual instances.

class Dog:

    # class attribute
    legs = 4

    def __init__(self, name):
        # instance attribute
        self.name = name

D1 = Dog('Rex')
D2 = Dog('Charlie')

print(f'{D1.name} has {D1.legs} legs.')
print(f'{D2.name} has {D2.legs} legs.')
Rex has 4 legs.
Charlie has 4 legs.

Using mutable types (lists, sets, dictionaries, other objects or classes) as class attributes may lead to unintended consequences, where a change in one object has an effect on all its siblings:

class Dog:

    tricks = []

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

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

D1 = Dog('Rex')
D2 = Dog('Charlie')

# teach some tricks to Rex
D1.addTrick('roll over')
D2.addTrick('play dead')

# Charlie learns tricks too!
print(D1.name, D1.tricks)
print(D2.name, D2.tricks)
Rex ['roll over', 'play dead']
Charlie ['roll over', 'play dead']

To keep a separate list for each object, use an instance attribute instead of a class attribute:

class Dog:

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

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

D1 = Dog('Rex')
D2 = Dog('Charlie')

# teach each dog a different trick
D1.addTrick('roll over')
D2.addTrick('play dead')

print(D1.name, D1.tricks)
print(D2.name, D2.tricks)
Rex ['roll over']
Charlie ['play dead']

Object inheritance

Objects can inherit properties from other objects. In the example below, the two child classes Cat and Dog are created by subclassing the parent class Pet:

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

class Dog(Pet):
    def bark(self):
        print(f'{self.name} is barking...')

class Cat(Pet):
    def meouw(self):
        print(f'{self.name} is meouwing...')

D = Dog('Rex')
C = Cat('Felix')

D.bark()
C.meouw()
Rex is barking...
Felix is meouwing...

Inheriting from multiple classes

A subclass can inherit from more than one parent class:

class Fish:
    name = 'fish'
    def swim(self):
        print(f'{self.name} is swimming…')

class Woman:
    name = 'woman'
    def sing(self):
        print(f'{self.name} is singing…')

class Mermaid(Fish, Woman):
    name = 'mermaid'

F = Fish()
F.swim()

W = Woman()
W.sing()

M = Mermaid()
M.swim()
M.sing()
fish is swimming…
woman is singing…
mermaid is swimming…
mermaid is singing…

Multiple inheritance levels

Multiple levels of inheritance are possible: derived classes can be used as base class for new subclasses, which can be used as base class for new subclasses, and so on.

class Parent:
    pass

class Child(Parent):
    pass

class GrandChild(Child):
    pass

class GrandGrandChild(GrandChild):
    pass

Too many inheritance levels can make your code very hard to debug. See object composition for an alternative approach to coupling between classes.

Overriding parent class attributes

Attributes and methods of a parent class can be overridden in child classes:

class Parent:
    hair = 'brown'
    eyes = 'green'

class Child(Parent):
    eyes = 'brown'

class GrandChild(Child):
    hair = 'blond'

parent = Parent()
child = Child()
grandchild = GrandChild()

print(f'parent: {parent.eyes} eyes, {parent.hair} hair')
print(f'child: {child.eyes} eyes, {child.hair} hair')
print(f'grandchild: {grandchild.eyes} eyes, {grandchild.hair} hair')
parent: green eyes, brown hair
child: brown eyes, brown hair
grandchild: brown eyes, blond hair

Checking instance type and parent class

Python has two built-in functions which are useful when working with classes: isinstance() and issubclass().

Use isinstance() to check if an object is an instance of a given class. The result will be True for any direct or indirect parent class.

print(isinstance(child, Child))
print(isinstance(child, Parent))
print(isinstance(child, GrandChild))
True
True
False

Use issubclass() to check if a class is a subclass of another class:

print(issubclass(Child, Parent))
print(issubclass(GrandChild, Child))
print(issubclass(Child, GrandChild))
True
True
False

Object composition

While inheritance provides a class with all properties of another class, as if they were their own, composition provides a class with a reference to another class or object which has some needed properties.

inheritance

Object of class B becomes an object of class A:

class A:
    def hello(self):
        print('hello!')

class B(A):
    pass

obj = B()
obj.hello()
hello!
composition

Object of class B has an object of class A:

class A:
    def hello(self):
        print('hello!')

class B:
    objA = A()
    def hello(self):
        self.objA.hello()

obj = B()
obj.hello()
hello!

Composition provides a looser approach to coupling between objects than inheritance. It allows a class to reuse the functionality of another class without copying its internal structure.

Polymorphism

Polymorphism is a technique in which the same method does different things in different objects.

In Python, polymorphism is usually implemented using an abstract base class, which contains only the definition of structure in the form of empty functions. Concrete classes are created by subclassing the abstract class and overriding its methods.

In the following example, both Dog and Duck have a .talk() method, but each implementation produces a different output:

class Animal:
    def talk(self):
        raise NotImplementedError("Subclass must implement abstract method.")

class Dog(Animal):
    def talk(self):
        print('woof!')

class Duck(Animal):
    def talk(self):
        print('quack!')

dog = Dog()
duck = Duck()

dog.talk()
duck.talk()
woof!
quack!

Duck typing

“If it looks like a duck and quacks like a duck, then it must be a duck.”

Duck typing is when we use the presence of an attribute or method to determine if an object can be used for a particular purpose, independent of its type.

normal typing
object suitability is determined by the object’s type
duck typing
object suitability is determined by the presence of certain methods or attributes

In the example below, it does not matter if the object is a Duck or a Butterfly, as long as it has a .fly() method:

class Duck:
    def fly(self):
        print("duck flying…")

class Butterfly:
    def fly(self):
        print("butterfly flying…")

class Whale:
    def swim(self):
        print("whale swimming…")

for animal in Duck(), Butterfly(), Whale():
    animal.fly()
Duck flying…
Airplane flying…
AttributeError: 'Whale' object has no attribute 'fly'

No dogs, cats, mermaids, children or ducks were harmed in the making of this page.

Last edited on 01/09/2021