Object Oriented Python
Object Oriented Programming with Python#
In python, everything is an object. Even functions are function objects.
The keyword class
is used to define eventual objects
Classes have attributes
, which are similar to variables but are defined inside a class and belong to that class.
Classes have methods
to give them abilities
When we use a class, the resulting object is an instance
.
Example#
class NewClass:
name_attribute = "Kenneth"
def name_method(self):
return self.name
new_instance = NewClass()
new_instance.name_method()
- By convention classes always start with a capital letter
- If they have multiple words in, then each first letter is capitalised
Most basic class#
class Thief:
pass
>>> from characters import Thief
>>> surfer190 = Thief()
>>> surfer190
<characters.Thief object at 0x101cbe278>
It is like running a function but classes don’t run. It creates an instance of the class.
Attributes#
class Thief:
sneaky = True
Access the class member with .
syntax
>>> surfer190.sneaky
True
You can also get the classes member with
>> Thief.sneaky
True
But instances are responsible for their own attribute values
>>> surfer190.sneaky = False
>>> surfer190.sneaky
False
Delete an instance of a class (object)
>>> del surfer190
Methods#
Methods are functions that belong to a class
Self#
Whenever they are used, they are used by an instance. Not the actual class.
That is why methods
always take at least one parameter that is the instance using the method
By convention that parameter is always called self
If you call the class method diretly you get an error unless you give it the instance:
>>> from characters import Thief
>>> surfer = Thief()
>>> surfer.pickpocket()
Called by <characters.Thief object at 0x101bbe2e8>
False
>>> Thief.pickpocket()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: pickpocket() missing 1 required positional argument: 'self'
>>> Thief.pickpocket(surfer)
Called by <characters.Thief object at 0x101bbe2e8>
True
Another thing is you can always refer to self
in the method:
import random
class Thief:
sneaky = True
def pickpocket(self):
print("Called by {}".format(self))
return bool(random.randint(0, 1))
Init#
When you create a new method python looks for a method called def __init__
Dunders indicate that it is a method python will run on it’s own. We need not run it ourselves.
def __init__(self, name, sneaky=True, **kwargs):
self.name = name
self.sneaky = sneaky
for key, value in kwargs.items():
# Very useful function
setattr(self, key, value)
when you initialise you will be required to provide a name
parameter
**kwargs
is a dictionary of key-value pairs
Class attributes#
Attributes set on the class are universal and can be changed just by specifying in .
notation
redcar.laps = 0
To prevent this you have to set the number of laps attribute in __init__
Inheritance#
You can inherit attributes and methods from a parent class by adding that class in the definition
class Thief(Character):
...
Note: Every class in python inherits from the built-in class called object
In python 2 you had to do this manually but python3 improved this to automatically inherit
Using the Super Class#
We can use methods on demand from the super class from the child class with super()
Usually done of overridden classes. So redefine the method and call super()
When using super()
Must include the method name and required arguments too.
Sub classes can take different arguments from their parent classes
class Character:
def __init__(self, name, **kwargs):
self.name = name
for key, value in kwargs.items():
setattr(self, key, value)
class Thief(Character):
sneaky = True
def __init__(self, name, sneaky=True, **kwargs):
'''
Super must be called first as sneaky could be defined in key word arguments
'''
super().__init__(name, **kwargs)
self.sneaky = sneaky
Refactoring#
Tech jargon for rearranging code nto a more logical state, by deleting, renaming, comibing and breaking code up.
Inferiting from multiple classes (Multiple Inheritance)#
You can inherit from multiple classes with an inheritance chain
.
The order that you inherit from classes is important because of super()
The MRO - Method Resolution Order
You can use class.__mro__
or inspect.getmro()
to look at your class’s method resolution order (MRO)
Tightly coupled code / design means that classes have to know a lot about other classes. Makes it harder to add in new functionality.
Loosely coupled code is the way to go, so it becomes easier to mix and match.
Useful functions#
isinstance('abc', str)
- Tells you whether something is an instance of a particular class
>>> isinstance(5.22, (int, str))
False
>>> isinstance(5.22, (int, float))
True
You can use a tuple
to check against
Easter Egg#
>>> isinstance(True, int)
True
>>> isinstance(True, bool)
True
issubclass(bool, int)
- Tells you whether a class is an instance of another class
>>> issubclass(Thief, Character)
True
>>> issubclass(bool ,int)
True
type('stephen')
- Tells you the type of object an instance is
Better to use isinstance
as it will check the full inheritance tree
There are a lot of dunder attributes __attribute__
myvar.__class__
- tells you the class of an instance
Can even get the class name:
>>> stephen = Thief('stephen')
>>> stephen.__class__
<class 'characters.Thief'>
>>> stephen.__class__.__name__
'Thief'
Python leans towards ducktyping where if a class looks and quacks like a duck it should be considered a duck. It is still handy to tell if instance of class is expected.
Another good one is dir(my_var)
which shows you available methods
Magic Methods#
Methods that python calls for you. We have seen __init__
already.
__str__
: Returns a string to identify object whenever it is turned into a string
__repr__
: Return official string representation, used for debugging, as much info as possible
__int__
/ __float__
: Return integer and float representations
>>> five = NumString(5)
>>> five
<numstring.NumString object at 0x101bbe278>
>>> str(five)
'5'
>>> int(five)
5
>>> float(five)
5.0
__add__
: Addition Math operation, from the left hand side
__radd__
: Reflective addition, addition from the right hand side
__iadd__
: which is +=
(i
stands for in place
)
You can check the full list in Emulating numeric types docs
class NumString:
def __init__(self, value):
self.value = str(value)
def __str__(self):
return self.value
def __int__(self):
return int(self.value)
def __float__(self):
return float(self.value)
def __add__(self, other):
return self.value + str(other)
def __radd__(self, other):
return self + other
def __iadd__(self, other):
self.value = self + other
return self.value
The len(my_obj)
is implemented with __len__
To check if item in my_obj
you use __contains__
If __contains__
does not exist, python uses __iter__
or __getitem__
def __iter__(self):
for item in self.slots:
yield item
Making an iterable#
What is yield
? It is very similar to return
but it does not immediately stop execution like return
does
yield
lets you send items out of the method as they are available and keep on working
This construct is called a generator
We can simplify above to:
def __iter__(self):
yield from self.slots
Because self.slots
is a list, python knows how to iterate through it.
When you use these default built-in methods with your custom class, it makes them easier to use and more what you are used to.
Custom verisions of built-ins#
When building custom classes you will usually make use of built-in
classes of your language.
The two most important of which are __init__
and __new__
__new__
- is for customising an immutable
data type
immutable
means the only time you should change them is at creation
time
Unlike init
, new
does a return
New does not take self
, as it is a special method that operates on a class
, not an instance
With immutable types, it can be unsafe to use super()
inside of __new__
, it is better to use parent method directly eg:
self = str.__new__(*args, **kwargs)
Take this code:
for _ in range(count):
....
The _
implies that you don’t care what that value is
And if you use copy.copy()
you are not using references, but copy by reference
Mimicking dot notation#
class javascriptObject(dict):
def __getattribute__(self, item):
try:
return self[item]
except KeyError:
return super().__getattribute__(item)
Another immutable example#
class Double(int):
def __new__(value, *args, **kwargs):
self = int.__new__(value, *args, **kwargs)
self *= 2
return self
Class Methods#
- Construct an instance of your class without having to have a class instance already
- They often work as a
factory
for an object - Don’t take
self
as their first argument, they take theclass
but that is a reserved keyword socls
is used (sometimesklass
is used) cls(books)
is basically calling the constructor__init__
@classmethod
is a decorator - a function that takes another function, does something with it then returns itclass Book: def __init__(self, title, author): self.title = title self.author = author def __str__(self): return '{} by {}'.format(self.title, self.author) class BookCase: def __init__(self, books=None): self.books = books @classmethod def create_bookcase(cls, book_list): books = [] for title, author in book_list: books.append(Book(title, author)) return cls(books)
Remember with classmethod you don’t call init, you just call cls()
Usage:
>>> bc = BookCase.create_bookcase([('Moby-Dick', 'Melville'), ('Jungle Book', 'Rudyard Kipling')])
>>> bc
<books.BookCase object at 0x1024bd470>
>>> bc.books
[<books.Book object at 0x1024bd5f8>, <books.Book object at 0x1024bd630>]
>>> str(bc.books[0])
'Moby-Dick by Melville'
Static Methods#
Static methods don’t require an instance or a class at all
Encapsulation (Hising elements of class away)#
Python motto: “We’re all adults here”
Just prepend the name of method or attribute with an underscore _
Double underscore __
makes method or underscore inaccessible outside of the class (private
)
So python
does name mangling on the attribute and methods
But you can find them by checking dir(obj)
Example#
class Protected:
__name = "Security"
def __method(self):
return self.__name
>>> from protected import Protected
>>> prot = Protected()
>>> prot.__name
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Protected' object has no attribute '__name'
>>> prot.__method
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Protected' object has no attribute '__method'
>>> dir(prot)
['_Protected__method', '_Protected__name', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__']
>>> prot._Protected__method
<bound method Protected.__method of <protected.Protected object at 0x1024bd668>>
>>> prot._Protected__method()
'Security'
>>> prot._Protected__name
'Security'
So in python nothing is truly ever locked away
Property decorator#
If you don’t want people to know it is a method, you can use the @property
decorator
class Circle:
def __init__(self, diameter):
self.diameter = diameter
@property
def radius(self):
return self.diameter / 2
So properties act as attributes
small = Circle(10)
print(small.radius)
But they cannot be set, as python does not know how to set it
small.radius = 15
But there is a way to create a setter
Use a decorator of the form @<property_name>.setter
. Then use the same property method but take in another parameter
class Circle:
def __init__(self, diameter):
self.diameter = diameter
@property
def radius(self):
return self.diameter / 2
@radius.setter
def radius(self, radius):
self.diameter = radius * 2
Implement Equality and Add#
class Die:
def __init__(self, sides=2, value=0):
if not sides >= 2:
raise ValueError("Must have at least 2 sides")
if not isinstance(sides, int):
raise ValueError("Sides must be a whole number")
self.value = value or random.randint(1, sides)
def __int__(self):
return self.value
def __eq__(self, other):
return int(self) == other
def __ne__(self, other):
return not int(self) == other
def __gt__(self, other):
return int(self) > other
def __lt__(self, other):
return int(self) < other
def __ge__(self, other):
return int(self) > other or int(self) == other
def __le__(self, other):
return int(self) < other or int(self) == other
def __add__(self, other):
return int(self) + other
def __radd__(self, other):
return int(self) + other
Usage#
>>> d6 = D6()
>>> d6.value
1
>>> d6 < 3
True
>>> d6 <= 1
True
>>> d6 <= 2
True
>>> d6 >= 2
False
>>> d6 >= 1
True
>>> d6 != 4
True
>>> d6 == 6
False
>>> d6 == 1
True
If you need to implement all of the methods try using the attrs library
Or if you just need equality and math ones then use the built-in functools.total_ordering
Interesting#
You can pass around a class Just like you would an integer or string
Just don’t call or initialise the class with ()
when passing it, unless you want result to be sent in