# Python OOP

## Summary


Python is an Object-Oriented Programming (OOP) language. Many OOP concepts are available in Python including classes, objects, (multiple) inheritance, polymorphism, and encapsulation.  Functions are also objects in Python. Data hiding in terms of access modifiers is not strict: class and instance attributes are public and private membership is not strictly enforced. 

Apart from instance methods, class methods and static methods are possible. Abstract classes are possible and they're equivalent to interfaces in other languages. Implementations of these typically employ decorators. Nested functions and nested classes are supported.  What's called operator overloading in OOP is implemented using magic methods. 

Since its beginning, Python support for OOP has continued to evolve. The aim has been to balance a simple intuitive syntax with features and performance desired by developers.

## Discussion

### What are some essential terms in Python OOP?

Here are a few essentials terms in Python OOP:  

    + **Class**: Bundles data and functionality together. Acts as a template or factory for creating new instances.
    + **Instance**: Created from a class. We say an instance's type is the class from which it's created. Eg. `point` is an instance of `Point` given that `point = Point()`. Many instances of a class can be created.
    + **Attribute**: Relevant to both classes and instances. Accessed using the dot notation `obj.name`. Attributes are either data attributes (eg. `point.x`) or methods (eg. `point.move_left(5)`).
    + **Method**: Belongs to an object. It's called to execute the code it contains. Note that `point.move_left` returns a *method object* whereas `Point.move_left` returns a *function object*.Instances are also called *objects*. Strictly speaking, we distinguish two types of objects: 

    + **Class Object**: Nothing more than a wrapper around the namespace created by the class definition. There are two things we can do with a class object: access its attributes or instantiate a new instance.
    + **Instance Object**: This is an instance created from a class. The only thing we can do with an instance object is to access its attributes.

### How do I define classes and instantiate objects in Python?

A class of name 'Foo' can be defined with `class Foo: pass`, where `class` is a Python keyword. An instance or object of this class can be created with `foo = Foo()`, where `foo` is an instance of the `Foo` class. 

Every Python class inherits from `object`. It's the base for all classes. It doesn't accept any arguments. It's also featureless, that is, we can't assign attributes to instances of `object`. 

A class `Rectangle` that inherits from another class `Shape` can be defined as `def Rectangle(Shape): pass`. **Multi-level inheritance** is also supported, such as, `def Square(Rectangle): pass`. **Multiple inheritance**, which is the ability of a class to inherit from multiple class, is also supported. For example, we can design a `Novel` class that inherits from two classes `Book` and `Fiction`: `def Novel(Book, Fiction)`. The order of the base classes is significant.

Unlike `foo` in the earlier example we can create classes that are initialized with arguments. For example, `corner = Point(1.3, -2.4)` creates a `Point` object named `corner` that's initialized with (x=1.3, y=-2.4) coordinates. 


### What's the lifecycle of a Python object?

When instantiated from its class definition, a Python object is created (memory is allocated) and then initialized. The object is stored in memory and accessible by the program. When the object is no longer required, it's destroyed and its memory is reclaimed by the system.

The following class methods are relevant to a Python object's lifecycle: 

    + `&lowbar;&lowbar;new&lowbar;&lowbar;`: Called automatically to instantiate an object. A developer rarely needs to customize the default implementation of this method.
    + `&lowbar;&lowbar;init&lowbar;&lowbar;`: Called automatically after `&lowbar;&lowbar;new&lowbar;&lowbar;` for initializing the object. Can accept arguments, such as, `def &lowbar;&lowbar;init&lowbar;&lowbar;(self, x, y)` of a `Point` class to initialize it's attributes.
    + `&lowbar;&lowbar;del&lowbar;&lowbar;`: Called automatically when the object is destroyed. A programmer can explicitly destroy the object by calling the `del()` built-in function or leave it to Python's garbage collector to do it. Either way, `&lowbar;&lowbar;del&lowbar;&lowbar;` is called. If the interpreter exits, there's no guarantee that `&lowbar;&lowbar;del&lowbar;&lowbar;` is called for existing instances.

### What's the naming convention for Python class definitions?

Python PEP 8 describes the naming convention and styling to be followed by developers. The following are relevant to class definitions: 

    + Surround top-level function and class definitions with two blank lines.
    + Method definitions inside a class are surrounded by a single blank line.
    + Write docstrings for all public modules, functions, classes, and methods.
    + `single_trailing_underscore_` argument name: avoids conflict with Python keywords. Eg. `register_student(self, class_, section)`.
    + `&lowbar;&lowbar;double_leading_underscore` attribute name: kind of private and avoids conflicts with subclasses. Invokes Python's name mangling rules.
    + `&lowbar;&lowbar;double_leading_and_trailing_underscore&lowbar;&lowbar;` name: convention is reserved for magic methods or other special uses. Don't name your attributes this way.
    + Class names should normally use the CapWords convention.
    + Class naming applies to Exceptions since they're also classes. Use the suffix "Error" in exception names.
    + Always use `self` for the first argument to instance methods.
    + Always use `cls` for the first argument to class methods.
    + For variable annotations, use single space after colon and no space before colon. Eg. `coords: Tuple[int, int]`.

### What are nested functions and nested classes in Python?

We typically define functions at module level. But it's possible to define a function within another function. For example, `def g(): pass` can be defined within the body of another function `f()`. Function `g` is therefore a **nested function**. It can't be invoked from outside its container function. It can be invoked only by `f`. Nested functions are therefore useful to implement encapsulation. For example, function `get_projection` is defined within function `get_area` but how the projection is calculated is particular to the area calculation. Therefore, we don't want others to use the projection.

It's possible for a nested function to be returned from its container function. In this case, the returned function can be called from outside. In fact, partial functions and closures are implemented this way. 

Like nested functions, for better encapsulation but at the expense of code reuse, a class can be defined within another class. The former is therefore a **nested class**. The outer class may instantiate objects of its inner classes. An inner class can be instantiated without instantiating its outer class.


### What are magic methods in Python OOP?

Magic methods, also called *dunder methods*, are special methods that are typically not explicitly called in application code. The Python interpreter calls them depending on the operation currently requested by the program.

For example, `&lowbar;&lowbar;new&lowbar;&lowbar;` and `&lowbar;&lowbar;init&lowbar;&lowbar;` are magic methods called respectively for creating and initializing an instance from its class. When we compare two objects (`a == b`), magic method `&lowbar;&lowbar;eq&lowbar;&lowbar;` is called on one object with the other object passed in as an argument. When we call `del()` method on an attribute, magic method `&lowbar;&lowbar;delattr&lowbar;&lowbar;` is called. To add two objects (`a + b`), magic method `&lowbar;&lowbar;add&lowbar;&lowbar;` is called. 

We may say that magic methods dictate what happens under the hood. They're useful when defining custom classes and we wish to override default behaviour. Custom classes can emulate the behaviour of built-in types. For example, we may define a custom string class derived from `str` built-in class. By overriding `&lowbar;&lowbar;eq&lowbar;&lowbar;`, we can enable case-insensitive comparison of strings.

All magic methods are named with leading and trailing double underscores. Other methods should not be named this way. 


### Could you compare a Python object's static methods and class methods?

When an instance is created, its methods are called instance methods. An instance method takes in `self` as the first argument. This points to that particular instance. Via this argument, the method can access all other attributes of the instance.

There are some methods that don't need the instance. They operate purely on the input arguments and output a result. They don't read or change state of the instance. These are called **static methods** and are defined via the `@staticmethod` decorator. In one use case, static methods could be helper functions that assist instance methods in their computations. They could also be called from outside the class. 

While each instance has its own state, we might need all instances share some attributes. For example, multiple instances of a `Logger` class log to the same file. Filename could be a shared state. Shared attributes are defined as class attributes rather than instance attributes. Methods that operate only on class attributes are called **class methods** and are defined via the `@classmethod` operator. A class method takes in `cls` as the first argument. 


### How do I define private or protected class attributes in Python?

Class and instance attributes are public. However, by naming convention, some developers treat attributes that start with a single underscore as protected. That is, only subclasses can access base class attributes. Others use single underscore to imply non-public access. The language itself offers no protection against public access. 

Another convention is to name attributes starting with double underscores. Such attributes are private to the class. In reality, Python does **name mangling**. It renames the attribute so that it becomes inaccessible from outside the class under its original name. However, within the class, the original name is valid. As an example, attribute `&lowbar;&lowbar;update` in class `Mapping` will become `_Mapping&lowbar;&lowbar;update`. 

Even with name mangling, a persistent developer can still access the attribute from outside the class by using the mangled name. In conclusion, Python's support for private attributes is not strict. Name mangling does solve the problem of conflicting names due to subclass attributes. 


### What's Method Resolution Order (MRO) in Python?

Consider `Novel` class that inherits from two classes `Book` and `Fiction`: `def Novel(Book, Fiction)`. If both `Book` and `Fiction` implement the method `get_title()` but `Novel` doesn't override this method, MRO determines which one gets called. Python adopts **C3 MRO algorithm**. Essentially, it says that method calls are resolved depth first but where a common ancestor is involved, it is called as late as possible. 

In the above `Novel` example, `Book.get_title()` is called. But let's consider a more complex example shown in the figure. Method call on a `Romantic19Novel` instance resolves to `RomanticNovel` and then `Novel` (depth first rule). However, `Novel` is also a base class of `NineteenthCenturyNovel`. Hence, `Novel` is deferred until later. Thus, the MRO is (`Romantic19Novel`, `RomanticNovel`, `NineteenthCenturyNovel`, `Novel`, `Book`, `object`).

Given any class, we can introspect the MRO by calling the `mro()` method, or accessing the `&lowbar;&lowbar;mro&lowbar;&lowbar;` attribute. Eg. `Romantic19Novel.mro()`. Note that these can't be called on instances.

While C3 MRO algorithm works in most cases, sometimes it's unable to resolve. In such a case, Python interpreter will raise an exception and fail to even define the class. 


### What is cooperative multiple inheritance in Python?

It's possible to implement our methods in such a way that MRO becomes easier. Even if the order of base classes were changed in the case of multiple inheritance, we would still achieve consistent runtime behaviour. Cooperative multiple inheritance also makes it easier to inject new classes into the inheritance hierarchy. 

Implementing methods cooperatively means fulfilling the following three conditions: 

    + Every implementation of the method need to call `super()`.
    + The method being called by `super()`needs to exist.
    + The caller and called base class methods have matching argument signatures. In particular, they accept `&ast;&ast;kwargs` keyword arguments dictionary as the last argument. A method will strip any keyword argument it requires and pass on the rest to its immediate base class via `&ast;&ast;kwargs`.


## Milestones

Jan  
1994

Python 1.0 is released. This version includes support for **classes and inheritance**, which were also present in earlier version 0.9. 

Dec  
2001

Python 2.2 is released. Via PEP 252 and 253, this release **unifies built-in types and user-defined classes**. We can now subclass built-in types. We can define **static methods** and **class methods**. We also have **properties** to automatically call methods when accessing or setting instance attributes. The introspection API is now unified across types and classes. Despite being a big change, this is done with backwards compatibility. Python 2.1 and earlier are now known as "classic Python" and classes without built-in types in their bases are called "classic classes". 

Jul  
2003

Python 2.3 is released. This version changes the Method Resolution Order (MRO). Specifically, it adopts **C3 MRO** that was invented back in 1996 for the Dylan programming language. Even with C3, there are some complex inheritance hierarchies where its impossible to resolve the order and Python raises an exception. 

Nov  
2004

Python 2.4 is released. Via PEP 318, this release includes **decorators for functions and methods**. Wrappers `staticmethod()` and `classmethod()` available since Python 2.2 are now available as decorators `@staticmethod` and `@classmethod`. **Class decorators** are introduced in Python 3.0 (Dec 2008) via PEP 3129. 

Apr  
2007

PEP 3119 is created to introduce **Abstract Base Class (ABC)** into the language. ABCs are conceptually close to what other languages call interfaces. In fact, back in 2001, PEP 245 considered introducing interfaces but the proposal was rejected. In December 2008, ABCs become part of Python 3.0 release and also backported to Python 2.6. 

Dec  
2008

Python 3.0 is released. Via PEP 3135, this release allows `super()` to be called without arguments to invoke methods. For example, the syntax `super(Foo, self).foo(1, 2)` is simplified to `super().foo(1, 2)`. The word `super` itself is not reserved and is interpreted specially only within the context of a method definition. This release also **gets rid of unbound methods**. Accessing a method as class attribute now results in a plain function object. 

Sep  
2012

Python 3.3 is released. Via PEP 3155, this release adds **qualified name** attribute `&lowbar;&lowbar;qualname&lowbar;&lowbar;` to classes and functions. This enhances introspection for nested classes and functions. The qualified name also helps in Python3 since there's no `im_class` attribute (unbound methods in Python2).