#theorie3_fonctions_et_objets.py #White #Dark #txt #download #pdf

        
#!coding: utf-8
from __future__ import print_function, division

#####################
# Base CINQ : Objets
### objets ##########

# Un objet permet de grouper plusieurs variables dans un... objet.
# Par exemple on peut grouper la "vie" et le "mana" dans un objet de type "Personnage".

# D'abord on définit la classe de l'objet ci-dessous.
# Pour l'instant elle est vide mais utilisable.
# Une classe va définir le "modèle" d'une série d'objet,
# par exemple tous les personnages seront des... personnages.

class Personnage:
    pass  # vide

# Pour créer un objet on écrit sa classe puis une paire de parenthèses.
# Vu que la classe est vide, il n'y a rien à mettre à l'intérieur de ces parenthèses.

bob = Personnage()
print(bob)  # "objet de la classe Personnage à l'adresse mémoire 0x..."

#
# Vu que bob est un objet, on peut lui mettre des variables !
bob.vie = 60 
bob.max_vie = 100
bob.mana = 10

# On peut créer d'autres personnages,
# comme alice, ou deux personnages sans noms :
alice = Personnage()
alice.vie = 40
alice.max_vie = 70
alice.mana = 50

# imaginez une liste de personnages !
les_personnages = [alice, bob]

alice.vie += 10
print(les_personnages[0].vie)  # 50
print(les_personnage[0] == alice)  # True

p = Personnage()
p.vie = 20
p.max_vie = 20
p.mana = 30

les_personnages.append(p)

p = Personnage()
p.vie = 25
p.max_vie = 25
p.mana = 20

les_personnages.append(p)

# Lancez [cet exemple](http://pythontutor.com/visualize.html#py=3&mode=display&code=class%20Personnage%3A%0A%20%20%20%20pass%20%20%23%20vide%0A%0A%23%20Pour%20cr%C3%A9er%20un%20objet%20on%20%C3%A9crit%20sa%20classe%20puis%20une%20paire%20de%20parenth%C3%A8ses.%0A%23%20Vu%20que%20la%20classe%20est%20vide%2C%20il%20n%27y%20a%20rien%20%C3%A0%20mettre%20%C3%A0%20l%27int%C3%A9rieur%20de%20ces%20parenth%C3%A8ses.%0A%0Abob%20%3D%20Personnage%28%29%0Aprint%28bob%29%20%20%23%20%22objet%20de%20la%20classe%20Personnage%20%C3%A0%20l%27adresse%20m%C3%A9moire%200x...%22%0A%0A%23%0A%23%20Vu%20que%20bob%20est%20un%20objet%2C%20on%20peut%20lui%20mettre%20des%20variables%20%21%0Abob.vie%20%3D%2060%20%0Abob.max_vie%20%3D%20100%0Abob.mana%20%3D%2010%0A%0A%23%20On%20peut%20cr%C3%A9er%20d%27autres%20personnages%2C%0A%23%20comme%20alice%2C%20ou%20deux%20personnages%20sans%20noms%C2%A0%3A%0Aalice%20%3D%20Personnage%28%29%0Aalice.vie%20%3D%2040%0Aalice.max_vie%20%3D%2070%0Aalice.mana%20%3D%2050%0A%0A%23%20imaginez%20une%20liste%20de%20personnages%20%21%0Ales_personnages%20%3D%20%5Balice%2C%20bob%5D%0A%0Aalice.vie%20%2B%3D%2010%0Aprint%28les_personnages%5B0%5D.vie%29%20%20%23%2050%0Aprint%28les_personnage%5B0%5D%20%3D%3D%20alice%29%20%20%23%20True%0A%0Ap%20%3D%20Personnage%28%29%0Ap.vie%20%3D%2020%0Ap.max_vie%20%3D%2020%0Ap.mana%20%3D%2030%0A%0Ales_personnages.append%28p%29%0A%0Ap%20%3D%20Personnage%28%29%0Ap.vie%20%3D%2025%0Ap.max_vie%20%3D%2025%0Ap.mana%20%3D%2020%0A%0Ales_personnages.append%28p%29)
# sur pythontutor, et observez la dynamique des flèches lors du deuxième <code>p = Personnage()</code>.
# Plus d'infos sur ces flèches dans la section <em>En savoir plus</em>.

# Essayez de créer une liste de 100 personnages différents en moins de 10 lignes de code.
# Remarquez qu'un personnage peut être différent d'un autre même s'il a les mêmes stats (vie, mana, ...).

#######################
# Base SIX : Fonctions 
### fonctions #########

# 
# On peut créer des fonctions, comme en math:

def f(x):  # f est une fonction d'un paramètre, appelé x
    return x + 1  # elle renvoie ce paramètre + 1

print(f(5))     # 6, les parenthèses APPELLENT la fonction
y = f(8)        # y = 9
a = 5
b = f(a+1) - 4  # b = (5+1) - 4 = 2
x = 2
z = f(x)        # z = 3
w = f(x+1)      # w = 4

# À quoi ça sert ?
# Certaines structures de code reviennent souvent,
# par exemple dans un exercice précédent, on a calculé le maximum d'une liste:

ma_liste = [1,2,7,2]

m = ma_liste[0]
i = 0
while i < len(ma_liste):
    if ma_liste[i] > m:
        m = ma_liste[i]
    i = i + 1
print(m)

# Ou encore, on aimerait donner <b>20</b> points de vie à un objet <code>Personnage</code>
# sans dépasser son maximum de vie:

bob.vie = bob.vie + 20
if bob.vie > bob.max_vie:
    bob.vie = bob.max_vie

# Cependant, on aimerait bien mettre ça dans une <em>boite</em>,
# une <em>boite à calculer le maximum d'une liste</em>,
# une <em>boite qui donne 20 points de vie sans dépasser le maximum</em>
# et pouvoir la réutiliser autant de fois que l'on veut.

#
# Nous allons donc faire une <strong>FONCTION</strong>:

def calculer_maximum(la_liste):
    m = la_liste[0]
    i = 0
    while i < len(la_liste):
        if la_liste[i] > m:
            m = la_liste[i]
        i = i + 1
    return m

#
# On a fait une fonction, avec un seul paramètre (la liste), et une valeur de retour (le maximum).

#
# Nous pouvons maintenant l'appeler:

une_belle_liste = [1,2,7,2]
a = calculer_maximum(une_belle_liste)

une_autre_liste = [8,0,1,6]
b = calculer_maximum(une_autre_liste)

# Pour appeler une fonction on...<ul>
# <li>écrit son nom,
# <li>ouvre la parenthèse,
# <li>met les paramètres séparés par des virgules,
# <li>ferme la parenthèse</ul>

# remarquez que la valeur de retour peut être stockée dans une variable...
b = calculer_maximum(une_autre_liste)

# ou utilisée dans une expression
c = calculer_maximum(une_belle_liste) + calculer_maximum(une_autre_liste)

# Lancez [cet exemple](http://pythontutor.com/visualize.html#code=def%20calculer_maximum%28la_liste%29%3A%0A%20%20%20%20%22%22%22%0A%20%20%20%20Calcule%20le%20maximum%20d'une%20liste%0A%20%20%20%20%0A%20%20%20%201%20Param%C3%A8tre%20d'entr%C3%A9e%20%3A%0A%20%20%20%20%20%20%20%20-%20la_liste%20%3A%20la%20liste%0A%20%20%20%201%20Param%C3%A8tre%20de%20retour%20%3A%0A%20%20%20%20%20%20%20%20-%20le%20maximum%0A%20%20%20%20%22%22%22%0A%20%20%20%20m%20%3D%20la_liste%5B0%5D%0A%20%20%20%20i%20%3D%200%0A%20%20%20%20while%20i%20%3C%20len%28la_liste%29%3A%0A%20%20%20%20%20%20%20%20if%20la_liste%5Bi%5D%20%3E%20m%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20m%20%3D%20la_liste%5Bi%5D%0A%20%20%20%20%20%20%20%20i%20%3D%20i%20%2B%201%0A%20%20%20%20%20%20%20%20%0A%20%20%20%20return%20m%0A%20%20%20%20%0Aune_belle_liste%20%3D%20%5B1,2,7,2%5D%0Aune_autre_liste%20%3D%20%5B8,0,1,6%5D%0A%0A%23%20on%20appelle%20la%20fonction%20via%20les%20parenth%C3%A8ses%0Aa%20%3D%20calculer_maximum%28une_belle_liste%29%0A%0A%23%20la%20valeur%20de%20retour%20peut%20%C3%AAtre%20stock%C3%A9e%20dans%20une%20variable...%0Ab%20%3D%20calculer_maximum%28une_autre_liste%29%0A%0A%23%20ou%20utilis%C3%A9e%20dans%20une%20expression%0Ac%20%3D%20calculer_maximum%28une_belle_liste%29%20%2B%20calculer_maximum%28une_autre_liste%29&cumulative=false&curInstr=0&heapPrimitives=false&mode=display&origin=opt-frontend.js&py=3&rawInputLstJSON=%5B%5D&textReferences=false)
# dans pythontutor !

# Les objets et fonctions vont bien ensemble,
# quand une fonction a accès à un objet elle peut modifier ses attributs:

def donner_potion(perso, montant):
    """Donne "montant" pv à un Personnage "perso" sans dépasser son maximum de vie"""
    perso.vie = perso.vie + montant
    if perso.vie > perso.max_vie:
        perso.vie = perso.max_vie
        
# on appelle la fonction avec nos personnages de la base CINQ
donner_potion(bob, 25)  # 25 points de vie pour bob
donner_potion(alice, 100)  # 100 points de vie pour alice

# Vous pouvez aussi voir [cet exemple](http://pythontutor.com/visualize.html#code=class%20Personnage%3A%0A%20%20%20%20pass%20%20%23%20vide%0A%0Abob%20%3D%20Personnage%28%29%0Abob.vie%20%3D%2060%20%0Abob.max_vie%20%3D%20100%0Abob.mana%20%3D%2010%0A%0Aalice%20%3D%20Personnage%28%29%0Aalice.vie%20%3D%2040%0Aalice.max_vie%20%3D%2070%0Aalice.mana%20%3D%2050%0A%0Adef%20donner_potion%28perso,%20montant%29%3A%0A%20%20%20%20%22%22%22Donne%20%22montant%22%20pv%20%C3%A0%20un%20perso%20%22perso%22%20sans%20d%C3%A9passer%20son%20maximum%20de%20vie%20%22%22%22%0A%20%20%20%20perso.vie%20%3D%20perso.vie%20%2B%20montant%0A%20%20%20%20if%20perso.vie%20%3E%20perso.max_vie%3A%0A%20%20%20%20%20%20%20%20perso.vie%20%3D%20perso.max_vie%0A%20%20%20%20%20%20%20%20%0A%23%20on%20appelle%20la%20fonction%20avec%20nos%20personnages%20de%20la%20base%20CINQ%0Adonner_potion%28bob,%2025%29%20%20%23%2025%20points%20de%20vie%20pour%20bob%0Adonner_potion%28alice,%20100%29%20%20%23%20100%20points%20de%20vie%20pour%20alice&cumulative=false&curInstr=0&heapPrimitives=false&mode=display&origin=opt-frontend.js&py=3&rawInputLstJSON=%5B%5D&textReferences=false)
# sur pythontutor

# Quand une fonction est appelée, python... <ul>
# <li> crée un bloc dans la mémoire "de gauche"
# <li> copie les paramètres comme si on avait fait "="
# <li> exécute le code de la fonction, qui peut faire tout ce qu'il veut
# <li> supprime le bloc quand on est à la fin</ul>

# Remarquez que la variable <code>i</code> et <code>m</code> sont <em>locales</em> à la fonction,
# elles sont crées dans la fonction et détruites à la fin.

## La boite noire

# <strong>EN UN MOT</strong>: une fonction, c'est une sorte de <em>boite noire</em>:<ol>
# <li>Elle a des paramètres d'entrée... ou pas,
# <li>fait un calcul,
# <li>et sort des valeurs de retour ... ou pas.</ol>

def f(x,y):
    return x + 2 * y

#
# Super dessin de la <em>boite noire</em>: 

# x,y -> [f] -> z

# Les fonctions peuvent avoir:<ul>
# <li>0 ou plus paramètres d'entrées (<em>paramètres</em> ou <em>arguments</em>).
# <li>0 ou plus paramètres de sortie (<em>valeur(s) de retour</em>).</ul>

## Exemples

#
# Voici plusieurs exemples de fonctions:

# 0 paramètre d'entrée, 0 de retour
def afficher_description():
    print("+------------------+")
    print("| Hello !          |")
    print("| Je suis Robert ! |")
    print("+------------------+")
    
afficher_description()

# 1 paramètre d'entrée, 0 de retour
def afficher_etoiles(nombre):
    for i in range(nombre):
        print("*")
        
afficher_etoiles(5)

# 2 paramètres d'entrée, 2 de retour
def minute_suivante(h,m):
    if m < 59:
        nouveau_h = h
        nouveau_m = m + 1
    else:
        nouveau_h = h + 1
        nouveau_m = 0
    return nouveau_h, nouveau_m

a,b = minute_suivante(13,30)
c,d = minute_suivante(12,59)

# pas d'entrée, un retour
def generate():
    liste = []
    for i in range(5):
        liste.append(i * i)
    return liste

l = generate()

## return

# Remarquez qu'on peut mettre autant de <code>return</code> que l'on veut,
# quand python lit un return, il arrête tout de suite la fonction et renvoie (ou non) une valeur.

def manger(x):
    if x < 0: # si x est négatif
        return -1  # la fonction s'arrête et renvoie -1
    
    i = 0
    s = 0
    while i < x:
        s += i
        i = i + 1
    return s

# Le mot clé <code>return</code>
# rappelle que le code <em>retourne</em> dans la fonction appelante.

###
# Pour en savoir plus (fonctions)
### pour-en-savoir-plus-fonctions, pour-en-savoir-plus-(fonctions)

## Paramètres par défaut

def f(x, y=5):
    return x + y

print(f(1))    # x=1, y=5
print(f(1,2))  # x=1, y=2

# Attention, ne mettre comme valeur par défaut que des objets <em>non modifiables</em> (<em>immutable</em> en anglais)
# comme des <code>int</code>, <code>str</code>, <code>tuple</code> mais pas 
# des <code>list</code> ni des objets classiques (la raison est expliquée plus bas).

#
# Un bon défaut est <code>None</code> qui est une valeur spéciale en Python :

def f(x, y=None):
    if y is None:
        y = x + 1
    return x + y

## Appel utilisant les noms des paramètres

def f(x, y):
    return x - y

print(f(5, 2))      # 3    
print(f(5, y=2))    # 3
print(f(x=5, y=2))  # 3
print(f(y=2, x=5))  # 3

## Docstring et interfaces

# Un des grand intérêt des fonctions est de les voir comme une boîte noire.
# Quand quelqu'un voit la première ligne (la <em>signature</em>) de la fonction,
# <code>def calculer_maximum(la_liste)</code>,
# il peut comprendre qu'il peut utiliser cette fonction pour calculer
# le maximum d'une liste <strong>sans en lire son code</strong>.

# Parfois, le nom à lui tout seul n'est pas suffisant pour bien utiliser
# la fonction, on y ajoute alors une <em>docstring</em>
# qui est un petit texte expliquant le <strong>rôle</strong> de la fonction.
# C'est généralement fait via <code>"""</code> et sur plusieurs lignes :

def calculer_maximum(la_liste):
    """
    Calcule le maximum d'une liste
    en utilisant une recherche linéaire.
    """
    m = la_liste[0]
    i = 0
    while i < len(la_liste):
        if la_liste[i] > m:
            m = la_liste[i]
        i = i + 1
    return m

# Une docstring doit donner des informations sur son <strong>rôle</strong>
# et non sur <em>comment</em> elle remplit son rôle.
# Sur <strong>ce</strong> qu'elle fait et non sur <em>comment</em> elle le fait.
# L'idée est de donner uniquement les informations pertinentes à la personne <strong>appelant</strong> la fonction,
# le reste étant des <em>détails d'implémentation</em>.

# Quand on a beaucoup de paramètres d'entrée et de retour dont les noms mérite explications,
# il est souvent utile de les lister dans la docstring :

def effectuer_un_combat(attaquant, defenseur, butin, lieu):
    """
    Effectue un combat entre une équipe attaquante et 
    une équipe en défense et renvoie la durée du combat.
    
    @param attaquant: l'équipe attaquante (de type Equipe)
    @param defenseur: l'équipe attaquante (de type Equipe)
    @param lieu: la position de la bataille (un tuple (latitude, longitude))
    @param butin: le nombre de pièce d'or volées à l'adversaire en cas de victoire (un int)
    
    @returns la durée du combat (un timedelta)
    """
    ...  # le code effectuant le combat!

# Indiquer les types d'arguments étant une tâche très récurrente et utile,
# python 3 introduit les annotations en parallèle de la docstring:

def effectuer_un_combat(attaquant:Equipe, defenseur:Equipe, butin:int, lieu:tuple) -> timedelta:
    """Effectue un combat entre une équipe attaquante et 
    une équipe en défense en une (latitude, longitude) donnée et renvoie la durée du combat."""
    ...  # le code effectuant le combat!

# Dans cet exemple, la docstring a pu être épurée
# les annotations ne doivent pas nécessairement être des types, ce code est possible:

def effectuer_un_combat(attaquant:Equipe, defenseur:Equipe, butin:int, lieu:(float, float)) -> timedelta:
    ...  # le code effectuant le combat!

# En python 3.7, à condition d'écrire
# <code>from __future__ import annotations</code>
# au début du fichier, les variables utilisées commme <code>Equipe</code> ou <code>timedelta</code> ne doivent même pas être déclarées.

## Simplifications automatiques

#
# Voici un aperçu de mon article obtenues grâce aux fonctions.

#
# (1) Les fonctions renvoyant un <code>bool</code> :

# avant
def f():
    ...
    if CONDITION:
        return True
    else:
        return False
    
# après (bool)
def f():
    ...
    return CONDITION

#
# Le return se lira alors <em>si et seulement si</em> (ssi).

#
# Par exemple:

# x est une voyelle ssi x == 'a' or x == 'e' or x == 'i' or x = 'o' or x = 'u'
def voyelle(x):
    return x == 'a' or x == 'e' or x == 'i' or x = 'o' or x = 'u'

# plus d'infos [ici](progra_equivalences.html#boolean-return)
# et [là](progra_equivalences.html#==-True).

#
# (2) Le if fonctionnel

# avant:
def f():
    ...
    if CONDITION:
        return X
    else:
        return Y
    
# après (if fonctionnel)
def f():
    ...
    return X if CONDITION else Y

# Cette transformation n'est pas obligatoire
# c'est juste un autre style de programmation.

# Plus d'infos [ici](progra_equivalences.html#if-variable)
# et [là](progra_equivalences.html#if-elif-cascade-on-variable)

#
# (2) Le return dans un if :

# avant
def f():
    if CONDITION:
        A
        return X
    else:
        B
        return Y
    
# après (return)
def f():
    if CONDITION:
        A
        return X
    B
    return Y

# A et B sont une suite de 0 ou plus instructions,
# je conseille de faire ceci si il y a peut d'instructions dans "A" et beaucoup dans "B".

# Plus d'infos
# [ici](progra_equivalences.html#if-return).

#
# (3) ∃ et ∀ :

# avant
def f():
    ...
    for X in L:
        if CONDITION:
            return True
    return False

# après : (voir fichier progra_functionals)
def f():
    ...
    return any(CONDITION for X in L)

# Lire ça comme <em>il existe un X dans L qui vérifie <b>CONDITION</b></em>
# ou <em>il existe un X dans L tel que <strong>CONDITION</strong></em>.

#
# Par exemple:
if any(x == 2 for x in L):
    ...

# Se lit <em>si</em> : <ul>
# <li> Il existe un <b>2</b> dans <code>L</code>.
# <li> <em>Il existe un x dans L tel que <code>x == 2</code></em>
# <li>∃ x ∈ L: x == 2</ul>

#
# Un conseil: passer à la ligne en fonction de la longueur dans le <code>any</code>!

#
# ∀ est le cas dual de ∃ :

# avant
def f():
    ...
    for X in L:
        if not CONDITION:
            return False
    return True

# après: (voir fichier progra_functionals)
def f():
    ...
    return all(CONDITION for X in L)

# On peut lire ça de plusieurs manières: <ul>
# <li> tous les X de L vérifient <b>CONDITION</b>
# <li> quel que soit X, il vérifie <strong>CONDITION</strong>
# <li> pour tous les éléments de X, il vérifie <strong>CONDITION</strong> </ul>

#
# Par exemple:
if all(x > 0 for x in L):
    ...
    
# Se lit <em>si</em> : <ul>
# <li> Tous les nombres de L sont > 0
# <li> Pour tous les élements x de L, on a x > 0
# <li> ∀ x ∈ L: x > 0 </ul>

# Plus d'infos [ici](progra_equivalences.html#for-if-return-True)
# et [là](progra_equivalences.html#for-if-return-False)

#
# (4+) Plus d'exemples dans mon article sur les équivalences.

## Récursivité

#
# Méditez là dessus avec pythontutor...

def f(x):
    if x > 0:
        f(x-1)
        print("Hello", x)
        f(x-1)

f(4)

## Variables locales

#
# Méditez là dessus sur pythontutor:

g = 5
p = 2
def f(x):
    print(g)
    y = x + 1
    p = 5
    print(p)

# <strong>LECTURE</strong>: Quand on lit une variable (print(x) par exemple), python fait ceci : <ul>
# <li> Regarder si la variable existe dans la Fonction en cours (dans le bloc à gauche, en bas sur pythontutor)
# <li> Si elle existe, il donne la valeur (contenu de la case), sinon :
# <li> Il regarde dans l'espace GLOBAL (global frame dans pythontutor), et s'il ne la trouve pas...
# <li> lance une NameError (variable non trouvée) </ul>

# <strong>ÉCRITURE</strong>: Quand on écrit dans une variable (variable = ), python fait ceci : <ul>
# <li> Regarder si la variable existe dans la Fonction en cours (dans le bloc à gauche, en bas sur pythontutor)
# <li> Si elle existe, il écrit la valeur (modifie le contenu de la case), sinon :
# <li> Il Crée la variable DANS LA FONCTION, on peut donc avoir une variable globale et locale avec le même nom</ul>

# Quand une fonction se termine (<code>return</code> ou arrivée à la fin du bloc),
# les variables sont détruites

# Même si c'est une mauvaise pratique, il est possible de dire qu'on parle d'une variable globale
# cela modifie donc la dynamique lors de l'écriture

g = 5
def f():
    global g
    g = 2

f()
print(g)

# comment éviter de marquer une variable comme "globale" ?
# En <strong>RENVOYANT</strong> quelque chose.

def f():
    return 2

g = f()

#
# Ou en groupant des variables dans des... <strong>OBJETS</strong> !

class Groupy:
    pass

def f(group):
    group.n = 2

group = Group()
group.n = 5

f(group)

print(group.n)

###
# Pour en savoir plus (objets)
### pour-en-savoir-plus-objets, pour-en-savoir-plus-(objets)

## Méthodes

# Rappelons nous de notre fonction qui prend un objet en paramètre
def donner_potion(perso, montant):
    """Donne "montant" pv à un Personnage "perso" sans dépasser son maximum de vie"""
    perso.vie = perso.vie + montant
    if perso.vie > perso.max_vie:
        perso.vie = perso.max_vie
        
# on appelle la fonction avec nos personnages de la base CINQ
donner_potion(bob, 25)  # 25 points de vie pour bob
donner_potion(alice, 100)  # 100 points de vie pour alice

# On peut mettre des fonctions dans les classes,
# on appelle ça des <em>méthodes</em>.

# À la place de le faire dans l'espace global, on le fait dans la classe
# Ces fonctions reçoivent <strong>TOUJOURS</strong> un premier paramètre "self"
# qui représente l'objet manipulé.

# Dans notre fonction <code>donner_potion(perso, montant)</code>
# le <code>self</code>, c'est <code>perso</code>

class Personnage:
    def boire_potion(self, montant):
        """Donne montant pv au Personnage sans dépasser son maximum de vie"""
        self.vie = self.vie + montant
        if self.vie > self.max_vie:
            self.vie = self.max_vie
            
    def crier(self):
        """Affiche un cri de guerre à l'écran"""
        print("Bouh!")

bob = Personnage()
bob.vie = 60 
bob.max_vie = 100
bob.mana = 10

alice = Personnage()
alice.vie = 40
alice.max_vie = 70
alice.mana = 50

bob.boire_potion(25)  # on appelle les méthodes comme ceci :)
alice.boire_potion(100)
alice.crier()

## Contructeur

# On voit des parties du code précédent qui sont répétées.
# Ce serait pratique de faire une fonction qui <em>initialise</em> les attributs.

def init_personnage(perso, a, b, c):
    perso.vie = a
    perso.max_vie = b
    perso.mana = c
    
bob = Personnage()
init_personnage(bob, 60, 100, 10)
alice = Personnage()
init_personnage(alice, 60, 100, 10)

# Cette fonctionnalité existe : Le constructeur.
# C'est une fonction qui est appelée à la création de tout objet.
# Le constructeur d'une classe s'appelle toujours <code>__init__</code>.

class Personnage:
    def __init__(self, a, b, c):
        self.vie = a
        self.max_vie = b
        self.mana = c
        
# On donne les paramètres à la création
bob = Personnage(60, 100, 10)
alice = Personnage(40, 70, 50)

## Héritage

# Méditez là dessus (pythontutor):

class Personnage:
    def __init__(self, vie, max_vie):
        self.vie = vie
        self.max_vie = max_vie
        
    def crier(self):
        """Affiche un cri de guerre à l'écran"""
        print("Bouh!")
        
    def boire_potion(self, x):
        self.vie = min(self.vie + x, self.max_vie)

class Guerrier(Personnage):  # un Guerrier est un Personnage
    def crier(self):  # mais qui fait "crier()" différemment
        print("Arrrrrrrrgggg")
    
class Magicien(Personnage):
    def crier(self):
        print("Wololo")

alice = Magicien(50, 100)
bob = Guerrier(75, 125)
guy = Personnage(20, 80)
personnages = [alice, bob, guy]  # différentes classes, mais tous des Personnages
for perso in personnages:
    perso.crier()                # va appeler une version différente en fonction de la classe
    perso.boire_potion(10)       # la fonction boire_potion existe toujours

#
# Pour réutiliser une fonction de la classe héritée, on fait comme ceci :

class Chou(Personnage):
    def crier(self):
        Personnage.crier(self)  # on appelle la version de "crier" qui se trouve dans "Personnage"
        print("Wouloulou")
      
class Marchand(Personnage):
    def __init__(self):  # on crée un Marchand sans paramètre
        Personnage.__init__(self, 50, 100)  # on appelle la version de __init__ dans Personnage
        self.argent = 10  # une nouvelle variable
    
    def crier(self):
        if self.argent > 0:
            print("Je suis un Marchand riche !")
        else:
            print("Je suis un Marchand fauché...")
        
denis = Chou(20, 40)
denis.crier()  # Affiche "Bouh!", puis "Wouloulou"

marc = Marchand()   # pas de paramètres vu que Marchand.__init__ n'a pas de paramètres
print(marc.vie)     # 50
print(marc.argent)  # 10
marc.crier()

# Comment ça se passe ?
# Python regarde si la classe de l'objet a la fonction appelée,
# si elle l'a, il l'appele,
# sinon, il regarde dans sa classe parente, et puis la parente de la parente.
# Et finalement si aucune des classes parentes n'a la fonction, il lance une exception.

#
# On peut savoir si un objet est d'une classe
print(isinstance(alice, Magicien))  # True
print(isinstance(bob, Magicien))    # False
print(isinstance(guy, Magicien))    # False
print(isinstance(guy, int))         # False

print(isinstance(alice, Personnage))  # True car héritage

# ou donner plusieurs classes pour éviter de faire un "or"
print(isinstance(alice, Magicien) or isinstance(alice, Guerrier))  # long
print(isinstance(alice, (Magicien, Guerrier)))  # raccourci

# bien que peu utile en pratique, on peut accéder à la classe d'un objet
cls = alice.__class__
print(cls == Magicien)    # True
print(cls == Personnage)  # False
print(type(alice) == alice.__class__)  # True 

# Veuillez toujours utiliser <code>isinstance</code> quand vous voulez tester l'appartenance,
# si votre code fait quelque chose pour un <code>Personnage</code>, ça devrait également
# marcher avec un <code>Magicien</code> ou un <code>Guerrier</code>.

## Références 

# Méditez là dessus avec pythontutor:

a = 5
b = a
a = 6
print(a)
print(b)

a = [1,2,3]
b = a
a[0] = 2
print(a)
print(b)

class Personnage:
    pass

a = Personnage()
b = a
a.vie = 1
print(a.vie)
print(b.vie)

# Plot twist : les listes sont des objets, de la classe <code>list</code>.
# Quand on crée un objet (et donc une liste),
# on reçoit une "référence" (symbolisée par une "flèche").

# Contrairement aux objets, les int, float, string n'ont pas de flèche,
# leur valeur est écrite directement dans la case.

# Les objets et références sont dans deux zones de la mémoire différentes,
# symbolisées par deux colonnes dans pythontutor.

# Quand on <strong>COPIE</strong> une variable, on copie la <strong>CASE</strong>,
# donc quand on copie un int, on copie le nombre.

# Quand on copie une "flèche", on fait une nouvelle flèche avec la même cible
# une <strong>COPIE</strong> se passe uniquement avec <code>x =</code>, <code>for x in</code> ou un appel de fonction :)

#
# Ainsi, dans le code précédent, on a plusieurs exemples de copie avec =

def modifier_premier(liste):
    liste[0] = 0

ma_liste = [1,2,3]
modifier_premier(ma_liste)

def modifier_local(x):
    x = 2

p = 2
modifier_local(p)