Skip to content
Snippets Groups Projects

Corrections - Tom

Merged Tom Reclik requested to merge tom into master
13 files
+ 59
44
Compare changes
  • Side-by-side
  • Inline

Files

+ 2
2
%% Cell type:markdown id: tags:
# Classes
So far, we have approached Python in a procedural way: Starting from a long list of statements, we have introduced functions to make our code more efficient, more reusable and also cleaner.
The functions took common datatypes, from single variables to dicionaries, as input and we could also return such entities.
Classes take this concept further and allow us to define our own entities (or objects). We distinguish between the "class" itself, this describes how this object looks like, which information it stores, or which functionality (defined by functions) it has. We can, informally, understand the class as a construction plan. If we want to make use of classes, we create "instances" of the class that then represent the actual objects.
For example, we may define a class "dog": This class allows us to describe the dog (e.g. fur colour, it's name or race), and what it can do (e.g. bark). The instances of this class then refer to specific dogs.
The general setup of a class is:
```
class class_name():
def __init__(self, <optional parameters>):
#... any initialisations we need to do....
def my_function_1(self, <optional parameters>):
# ... define the function ....
# with or without return value
#... define more functions if needed ...
```
**We notice:**
* The class is defined by the keyword ```class```, followed by the name of the class
* Each class needs to be initialised, this is done in the function defined with ```___init___()```. The function with a double underscore before and after the keyword is called a "dunder" or "magic" method in python. These methods are not intended to be called directly (by us), but they are typically called internally by some other method or action. In our case, this "magic" method is automatically called when we create an instance of this class. This initialisation function can take one or more optinal arguments. Typically, we define variables here that define the properties of our class.
* Each class needs to be initialised, this is done in the function defined with ```___init___()```. The function with a double underscore before and after the keyword is called a "dunder" or "magic" method in python. These methods are not intended to be called directly (by us), but they are typically called internally by some other method or action. In our case, this "magic" method is automatically called when we create an instance of this class. This initialisation function can take one or more optional arguments. Typically, we define variables here that define the properties of our class.
* The indicator ```self``` indicates the instance of this class. It is not a keyword as such (we could use some other word) - but pretty much everyone uses *self* to avoid confusion. We use this to refer to, e.g., the variables or functions that are part of this class.
* A class can have one or more functions that define the functionality. They can (or not) have return values, they may operate on internal variables of the class, etc.
A simple class for dogs could be:
%% Cell type:code id: tags:
``` python
class Dog():
def __init__(self):
pass
def bark(self):
print('Woof')
```
%% Cell type:markdown id: tags:
Note that here we do not have anything to initialise - but we still need the magic ```__init___()``` function. We can use the keyword ```pass``` to tell python to "pass over" this function and move on.
We also note that the function "bark" needs to take ```self``` as an argument - we need to tell Python, even though the code is indented below the class, that this method is part of the class.
> We also used upper case letters to define the class. This is a common convention.
We can then create an instance of the class to refer to a specific dog and let it bark:
%% Cell type:code id: tags:
``` python
# create an instance
dog = Dog()
# bark
dog.bark()
```
%% Output
Woof
%% Cell type:markdown id: tags:
This is not the most exciting dog... Let's add some variables
%% Cell type:code id: tags:
``` python
class Dog():
def __init__(self, name, fur_color, clean = True):
self.name = name
self.fur_color = fur_color
self.clean = clean
def bark(self):
print('Woof')
```
%% Cell type:markdown id: tags:
Here, we added a few variables, making the last one optional with a default argument.
The variables we created are associated with the class again by using ```self``` beforehand.
**Private and public variables**
> All variables in python are public.
All variables that are part of the class are public, i.e. we cannot prevent someone to access them directly an change them. For example:
All variables that are part of the class are public, i.e. we cannot prevent someone to access them directly and change them. For example:
%% Cell type:code id: tags:
``` python
# create an instance of the class
dog = Dog('Rocky', 'brown')
dog.bark()
print('The name of the dog is {}'.format(dog.name))
```
%% Output
Woof
The name of the dog is Rocky
%% Cell type:markdown id: tags:
However, this is not really considered good practice - we should avoid to temper with the internals of the class directly but instead write "setter" and "getter" methods.
The "setter" methods take an argument and set the respective variables to the new value, for example "set_name(new_name)" should update ```self.name``` to ```new_name```.
The "getter" methods should return the value of the variable, e.g. ```get_name()``` or just ```name()```.
### Exercise
Extend the class definition by a "getter" method ```name()``` that returns the name of the dog, so that we can write:
```print('The name of the dog is {}'.format(dog.name()))```
> Trying to avoid accesing class variables avoids potential bugs a little as we interact with the variables in a controlled way.
%% Cell type:code id: tags:
``` python
class Dog():
def __init__(self, name, fur_color, clean = True):
self.name = name
self.fur_color = fur_color
self.clean = clean
def bark(self):
print('Woof')
# ... your code here ...
```
%% Cell type:code id: tags:
``` python
dog = Dog('Rocky', 'brown')
#print('The name of the dog is {}'.format(dog.name()))
```
%% Cell type:markdown id: tags:
**Getting help**
Python has an inbuilt function that provides more information about objects and classes. You can access it anytime using
> help(object)
%% Cell type:code id: tags:
``` python
help(dog)
```
%% Output
Help on Dog in module __main__ object:
class Dog(builtins.object)
| Dog(name, fur_color)
|
| Methods defined here:
|
| __init__(self, name, fur_color)
| Initialize self. See help(type(self)) for accurate signature.
|
| bark(self)
|
| is_clean(self)
|
| play(self)
|
| wash(self)
|
| ----------------------------------------------------------------------
| Data descriptors defined here:
|
| __dict__
| dictionary for instance variables (if defined)
|
| __weakref__
| list of weak references to the object (if defined)
%% Cell type:markdown id: tags:
### Exercise
Imagine we are a bank and need to establish a way for us to track the transactions with customers.
In a very simple scenario, we need an account for each customer, that has the following properties:
* It is associated with a customer with their name.
* For simplicity, we only consider euros (i.e. no cents, just integer values).
* Customers can put money into the account, withdraw money and ask for the balance.
* The bank does not offer credit or overdraft.
Implement this as a class.
First, think about why the above requirements are ambiguous and not sufficient (and then come up with a way to mitigate this.)
Then, think about which cases you need to consider in your implementation.
*Hints*
* The function ```int(number)``` converts number to an integer.
* If we need to return "nothing", we can use the keyword ```None```.
%% Cell type:code id: tags:
``` python
# ... your code here ...
```
%% Cell type:code id: tags:
``` python
# Test your account
account = Account('Smith')
print('Opened account for {}'.format(account.name()))
print('Current balance: {}'.format(account.balance()))
# make a deposit
account.deposit(10)
print('Current balance: {}'.format(account.balance()))
# try to make an invalid deposit -- should produce an error message
account.deposit(-10)
# try to make an invalid deposit -- should produce an error message
account.deposit(10.5)
# try to make an invalid deposit -- should produce an error message
account.deposit('ten')
# withdraw some money
my_money = account.withdrawal(5)
print('I have now {} euros in my hand'.format(my_money))
print('Current balance: {}'.format(account.balance()))
# try to make an invalid withdrawal -- should produce an error message and we have "nothing" in our hand
my_money = account.withdrawal(-10)
print('I have now {} euros in my hand'.format(my_money))
# try to make an invalid withdrawal -- should produce an error message and we have "nothing" in our hand
my_money = account.withdrawal(10.5)
print('I have now {} euros in my hand'.format(my_money))
# try to make an invalid withdrawal -- should produce an error message and we have "nothing" in our hand
my_money = account.withdrawal('ten')
print('I have now {} euros in my hand'.format(my_money))
# try to make an invalid withdrawal -- should produce an error message and we have "nothing" in our hand
my_money = account.withdrawal(500)
print('I have now {} euros in my hand'.format(my_money))
```
%% Cell type:markdown id: tags:
***Note***
In our implementation so far, we have resorted to a rather crude error handling by printing out an error statement.
This is already quite good as we test for unexpected behaviour - but it would be better to deal with the erronous cases in a different way. This is called "exception handling" which we will focus on later.
Loading