To quote one of the first sentences from the wikipedia page on Python:
Python supports multiple programming paradigms, including object-oriented, imperative and functional programming or procedural styles.
In the Introductory notes I also referred to modules functions as well as member functions.
These bits of jargon are related; everything in Python is an object.
In the context of programming, objects
“…are data structures that contain data, in the form of fields, often known as attributes; and code, in the form of procedures, often known as methods.” https://en.wikipedia.org/wiki/Object-oriented_programming
Every entity in a python script has both data associated with it, and functions.
For example, even the most basic seeming data is an object:
print( (1).__class__ )
yields int
- ie the class name attribute of the number 1 is int
(the parentheses are needed to distinguish the “.” from a decimal sign!).
Note that this is different from basic programming concept of data type!
As proof, the full list of member functions for our number are:
bit_length denominator imag real
conjugate from_bytes numerator to_bytes
i.e. functions to e.g. get the real and imaginary parts of the number, as well as return a “bytes” (binary) representation.
As well as these member functions, as with most Python objects there are a host of hidden member functions:
__abs__ __init__ __rlshift__
__add__ __int__ __rmod__
__and__ __invert__ __rmul__
__bool__ __le__ __ror__
__ceil__ __lshift__ __round__
__class__ __lt__ __rpow__
__delattr__ __mod__ __rrshift__
__dir__ __mul__ __rshift__
__divmod__ __ne__ __rsub__
__doc__ __neg__ __rtruediv__
__eq__ __new__ __rxor__
__float__ __or__ __setattr__
__floor__ __pos__ __sizeof__
__floordiv__ __pow__ __str__
__format__ __radd__ __sub__
__ge__ __rand__ __subclasshook__
__getattribute__ __rdivmod__ __truediv__
__getnewargs__ __reduce__ __trunc__
__gt__ __reduce_ex__ __xor__
__hash__ __repr__
__index__ __rfloordiv__
Why the funny notation for these member functions?
Unlike e.g. C++ or Java, in Python there is no mechanism to declare member functions as private vs public (or protected) – this jargon is all to do with OOP, specifically one of the core concepts known as inheritance. More on that below!
Instead, in Python the notion of private member functions is replaced by hidden member functions; technically they’re not private and can be overridden in child classes, but it is suggested by the authors of the class that you don’t!
If you were to open an IPython interactive shell and type print.__
and then
hit tab you’d see that even the print
function is an object. Functions
are objects too!
So now that we’ve established that even “simple” numbers and functions are all objects, lets look a bit more at what an object is.
There are a few main concepts at the heart of OOP, including
Ok? Probably not! We have so far answered a question with a bunch of jargon. Let’s make this a little clearer with an example.
As an example, let’s consider that we’re writing a program that works with geometric shapes like circles, squares, triangles etc.
For each type of shape we might want to calculate things like perimeter, area, bounding box etc.
The OOP way to solve this task would be to first of all create a parent class that contains the features common to all shapes. For example
class Shape():
pass
would define a class called Shape (the pass
statement is needed
as we need at least one line of code in the class-block).
Next, we could add in some member attributes - data associated with our class - and member functions, (i.e. the class will encapsulate all required functionality).
class Shape():
name = "Generic shape"
centre = []
def print_name(self):
print(self.name)
def get_area(self):
return None
Here we have 2 member attributes, and 2 member functions.
You might have spotted the funny (required!) first argument in the
definition of the member functions; we called it self
, which is
a convention, as it is a variable holding the object itself.
This is how we have access an object’s other attributes and member
functions, inside a member function.
The member function called get_area
returns a None
, because
a generic shape doesn’t exist, and can’t have an area!
Now, as we said, generic shapes don’t exist, we only have concrete shapes! So let’s create a very simple first child class; the square
class Square(Shape):
name = "Square"
The Shape
in the parentheses of the class
definition line, mean that the
Square
class will inherit from the Shape
class.
If we create an object of class Square, it will automatically have inherited
the print_name
member function:
square1 = Square()
square1.print_name()
prints Square
to the console.
Similarly, we might want to have a triangle
class Triangle(Shape):
name= "Triangle"
Now at the moment, except for printing out the correct name, all of
our classes will return None
when calling get_area
. But, for squares
and triangles, we should be able to get a meaningful value!
Now we need to add in some child-class specific code.
First of all, when creating a square , we need to know a single dimension
for the size. To achieve this we add a member attribute, width
,
and over-write the get_area
function:
class Square(Shape):
name = "Square"
width = 0 # A default value
def get_area(self):
return width * width
Similarly for the triangle
class Triangle(Shape):
name = "Triangle"
width = 0
height = 0
def get_area(self):
return 0.5 * width * height
Almost there! We’ve added some child-class specific functionality now so that each concrete shape class should report the correct area.
But wait, we haven’t created a good way to assign the required data yet.
The full OOP approach would include creating setter and getter functions for each attribute that we want anyone using the class to be able to access.
However, for brevity, lets override the default constructor
function (called when an object of a class is created). In Python
that function is named __init__
.
class Square(Shape):
name = "Square"
width = 0 # A default value
def __init__(self, width):
self.width = width
def get_area(self):
return width * width
class Triangle(Shape):
name = "Triangle"
width = 0
height = 0
def __init__(self, width, height):
self.width = width
self.height = height
def get_area(self):
return 0.5 * width * height
Now, regardless of what shape we’re dealing with, we
can interrogate the area using get_area
and receive
the correct response!
This is closely related to polymorphism as we can call the
same function on either child class and get meaningful output.
NOTE: as we specified width
and height
as positional arguments,
Triangles
and Squares
must now always be created with the width
and
width
and height
values as inputs (respectively).
We have not covered composition in the above treatment, as this most commonly emerges when dealing with more complex classes. Nonetheless, this gives a basic introduction to classes and OOP with Python, and hopefully sheds some light on the “why” of member-functions!