Objects ↩
- Introduction
- Objects in Python
- Classes and instances
- Object inheritance
- Checking instance type and parent class
- Object composition
- Polymorphism
- Duck typing
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:
- defining the class
- 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.