Appendix - Worked out example: accounts

Appendix - Worked out example: accounts

Classes and objects

First let us create a class representing bank accounts ("comptes en banque"):

class Compte :

    def __init__(self, titulaire) :
        self.titulaire = titulaire
        self.solde = 0

Now let us create an object of this class, representing someone's bank account:

>>> a = Compte("kim")
>>> print(a.titulaire)
kim
>>> print(a.solde)
0
>>> a.solde = 10
>>> print(a.solde)
10
>>> a.solde += 1000
>>> print(a.solde)
1010

Hiding instance variables

Of course, we don't like it too much that our bank account details are so easily accessible from the outside. So let us try to hide the attributes:

class Compte :

    def __init__(self, titulaire) :
        self.__titulaire = titulaire
        self.__solde = 0

As you can see, you cannot easily access the instance attributes of an object of this class, such as an account's balance, anymore:

>>> a = Compte("kim")
>>> a.__titulaire
AttributeError: 'Compte' object has no attribute 'titulaire'
>>> a.__solde
AttributeError: 'Compte' object has no attribute '__solde'

Accessor methods

But oh, wait a minute, we need to be able to at least get access to it from the inside, so we need accessor methods that allow us to access these values, and while we are at it let's add a method to be able to print the account too:

class Compte :

    # initialiser
    def __init__(self, titulaire):
        self.__titulaire = titulaire
        self.__solde = 0

    # accessor
    def titulaire(self):
        return self.__titulaire

    # accessor
    def solde(self):
        return self.__solde

    # print representation
    def __str__(self) :
        return "Compte de {} : solde = {}".format(self.titulaire(),self.solde())

Remember that when we define a method we write def solde(self): and shouldn't forget the self parameter. But when we call a method on self (or on another object) we write `self.solde() without the self-parameter and Python will implicitly bind the self-parameter to the receiver object.

If you don't know or don't remember how the format() method works on strings, look it up, it's not so crucial for this example; we could easily have written the __str__ method without it, but it leads to more compact code.

Also note how we use the accessors methods titulaire() and solde() in the __str__ method as well. This makes it easier to change the internal variable if we want to.

>>> a = Compte("kim")
>>> print(a)
Compte de kim : solde = 0
>>> print(a.titulaire())
kim

Mutator methods

Objects carry their own state and can provide their own methods to manipulate that state. We will now add two such mutator methods (they are called like that since they mutate the state of the object); one for redrawing money from the account, and another to deposit money on the account:

class Compte :

    # initialiser
    def __init__(self, titulaire):
        self.__titulaire = titulaire
        self.__solde = 0

    # accessor
    def titulaire(self):
        return self.__titulaire

    # accessor
    def solde(self):
        return self.__solde

    # string representation
    def __str__(self) :
        return "Compte de {} : solde = {}".format(self.titulaire(),self.solde())

    # *** No modifications above! Only the methods below were added... ***

    # mutator
    def deposer(self, somme):
        self.__solde += somme
        return self.solde()

    # mutator
    def retirer(self, somme):
        if self.solde() >= somme :
            self.__solde -= somme
            return self.solde()
        else :
            return "Solde insuffisant"

Now we can add or remove money from an account with the newly added methods:

>>> compte_charles = Compte("Charles")
>>> print(compte_charles)
Compte de Charles : solde = 0
>>> print(compte_charles.deposer(100))
100
>>> print(compte_charles.retirer(90))
10
>>> print(compte_charles.retirer(50))
Solde insuffisant
>>> print(compte_charles.titulaire())
Charles

Class variables

While an object's instance variables carry the state of the object that is specific to each particular instance, sometimes it is also useful to have a state that is shared by all the objects of a same class. For example, all account objects may share the same interest rate. Such shared state common to all instances of a same class can be declared in a class variable, or class attribute, defined within the class:

class Compte :

    # class variable
    taux_interet = 0.02

    def __init__(self, titulaire):
        self.__titulaire = titulaire
        self.__solde = 0

    def titulaire(self):
        return self.__titulaire

    def solde(self):
        return self.__solde

    # We also modify the print representation to show the interest rate
    def __str__(self) :
        return "Compte de {0} : solde = {1:4.2f} \ntaux d'intérêt = {2}".format(self.titulaire(),self.solde(),self.taux_interet)

    def deposer(self, somme):
        self.__solde += somme
        return self.solde()

    def retirer(self, somme):
        if self.solde() >= somme :
            self.__solde -= somme
            return self.solde()
        else :
            return "Solde insuffisant"

Two different instances of this class share the same value for the class variable, but their instance variables may vary:

>>> compte_kim = Compte("Kim")
>>> print(compte_kim)
Compte de Kim : solde = 0.00
taux d'intérêt = 0.02
>>> compte_siegfried = Compte("Siegfried")
>>> print(compte_siegfried)
Compte de Siegfried : solde = 0.00
taux d'intérêt = 0.02

Changing the state of the class variable changes it for all instances of that class:

>>> Compte.taux_interet = 0.04
>>> print(compte_kim)
Compte de Kim : solde = 0.00
taux d'intérêt = 0.04
>>> print(compte_siegfried)
Compte de Siegfried : solde = 0.00
taux d'intérêt = 0.04

Shadowing

Attention! It is possible for an instance variable to have the same name as a class variable. Here, we add a new instance variable to an object that will shadow the value of the class variable.

>>> compte_kim.taux_interet = 0.03

Asking the object for that variable will now return the value of the newly assigned instance variable:

>>> print(compte_kim.taux_interet)
0.03

Even though the class variable still exists with its old value: newly assigned instance variable:

>>> print(Compte.taux_interet)
0.04

Asking other objects of this class for the value of that variable will still return the value of that class variable (in these other objects, the class variable wasn't shadowed by an instance variable.

>>> print(compte_siegfried.taux_interet)
0.04

Hiding class variables and class methods

Now that we have seen how to create a class variable, we can ask ourselves the question whether there is also a way to hide a class variable so that we cannot change it from the outside?

The answer is yes: add an __ to the name, just like we did with the instance variables. But then we also need to add an accessor and a mutator method if we want to read or write the class variable externally.

These need to be declared as class methods. Class methods are methods that should be invoked on the class, not on the instance.

class Compte :
    __taux_interet = 0.02

    @classmethod
    def taux_interet(cls):
        return cls.__taux_interet

    @classmethod
    def set_taux_interet(cls,nouveau_taux):
        cls.__taux_interet = nouveau_taux

    def __init__(self, titulaire):
        self.__titulaire = titulaire
        self.__solde = 0

    def titulaire(self):
        return self.__titulaire

    def solde(self):
        return self.__solde

    def __str__(self) :
        return "Compte de {0} : solde = {1:4.2f} \ntaux d'intérêt = {2}".format(self.titulaire(),self.solde(),self.taux_interet())

    def deposer(self, somme):
        self.__solde += somme
        return self.solde()

    def retirer(self, somme):
        if self.solde() >= somme :
            self.__solde -= somme
            return self.solde()
        else :
            return "Solde insuffisant"

Note how the class methods take an extra parameter which, by convention, is named cls and that refers to the class, just like normal instance methods took an extra parameter self that refer to the receiving object.

>>> compte_kim = Compte("Kim")
>>> Compte.taux_interet()
0.02

Note that, in Python, you can invoke the class method on the instance too! That may be a bit confusing, but what happens is that Python first tries to send the method to the object instance, and if it cannot find an instance method with that name it will invoke it instead as a class method on the class of that instance.

>>> compte_kim.taux_interet()
0.02

Inheritance

Now let us consider a special kind of account, a checkings account, which inherits from the general account type and adds one additional method.

Inheritance is one of the core concepts of object-oriented programming. It enables the creation of a new (refined) class from an existing one, called its parent class. The new class, called subclass or child class, inherits all attributes and methods of the existing one.

To indicate that a class inherits from a another one, put the name of the parent class in parenthesis after the class name:

class CompteCourant(Compte) :

    def transferer(self,compte,montant) :
        res = self.retirer(montant)
        if res != "Solde insuffisant" :
            compte.deposer(montant)
        return res

This is the complete definition of the checkings account class CompteCourant. All other methods and attributes are simply inherited from the account superclass Compte.

>>> compte_kim = CompteCourant("Kim")
>>> compte_charles = CompteCourant("Charles")
>>> compte_kim.deposer(100)
>>> compte_kim.transferer(compte_charles,50)
>>> print(compte_kim.solde())
50
>>> print(compte_charles.solde())
50
>>> print(compte_kim.transferer(compte_charles,60))
Solde insuffisant

Method overriding

Also we can redefine existing methods such as the method for withdrawing money that charges 0.10 Euro extra for every cash withdrawal.

class CompteCourant(Compte) :
    __frais_retirer = 0.10

    def transferer(self,compte,montant) :
        res = self.retirer(montant)
        if res != "Solde insuffisant" :
            compte.deposer(montant)
        return res

    def retirer(self, somme):
        return Compte.retirer(self, somme + self.__frais_retirer)

Note how this method retirer overrides a method with the same name already defined in the superclass Compte. In fact, for its implementation, this method makes use of the method defined on that superclass, by explicitly calling the method on that class and passing self as argument. This seems to have the desired effect:

>>> compte_kim = CompteCourant("Kim")
>>> print(compte_kim.deposer(1000))
1000
>>> print(compte_kim.retirer(10))
989.9
>>> print(compte_kim.retirer(10))
979.8

Super call

The above implementation of the method retirer seems to work, but the explicit call to Compte.retirer could be avoided.

So why not write the method with a self-call like this?

def retirer(self, somme):
    return self.retirer(somme + self.__frais_retirer)

If you would try that, you would get the following error upon calling that method:

RecursionError: maximum recursion depth exceeded

The reason is that, rather than calling the method on the superclass, the method would call itself (which would recursively call itself, and so on).

However, there is a better way to call the method on the super class, by using a super call with the special method super():

class CompteCourant(Compte) :
    __frais_retirer = 0.10

    @classmethod
    def frais_retirer(cls):
        return cls.__frais_retirer

    def __init__(self, titulaire,banque) :
        super().__init__(titulaire)
        self.__banque = banque

    def retirer(self, somme):
        return super().retirer(somme + self.frais_retirer())

    def __str__(self) :
        return super().__str__() + "; banque = " + self.__banque

In fact, writing

def retirer(self, somme):
    return super().retirer(somme + self.frais_retirer())

is equivalent to writing

def retirer(self, somme):
    return Compte.retirer(self, somme + self.__frais_retirer)

but has the advantage of referring to the superclass implicitly, rather than having to refer to it explicitly.

Also note how we extended the class definition with two other super calls. One in the __init__ method to initialise an additional instance variable representing the name of the bank, and another in the __str__ method. Both of these methods are defined in terms of their corresponding methods on the superclass, by making a super call.

Here is an example that shows this new class definition at work:

>>> compte_kim = CompteCourant("Kim","ING")
>>> print(compte_kim.deposer(1000))
1000
>>> print(compte_kim.retirer(10))
989.9
>>> print(compte_kim.retirer(10))
979.8
>>> print(compte_kim)
Compte de Kim : solde = 979.80
taux d'intérêt = 0.02; banque = ING

Page précédente Page suivante
<string>