# Structuration du code

## Les fonctions

- La définition d’une fonction se fait par le mot clé def suivi du nom de la fonction.

- Les éventuels paramètres sont déclarés entre parenthèses.

- Le caratère “:” délimite le début de la fonction.

- Le bloc de la fonction est délimité par les indentations.

Premier exemple:

    >>> def say_hello(firstname, lastname):
    ...    print ("Hello {} {}".format(firstname.title(), lastname.title()))
    ...
    >>> say_hello('Terry', 'gilliam')
    Hello Terry Gilliam

Il n’y a pas de distinction entre les fonctions et les procédures en Python. Les procédures sont des fonctions qui ne retournent pas de résultat.

Pour retourner un résultat on utilise la directive return.

    >>> def get_hello(firstname, lastname):
    ...     return "Hello {} {}".format(firstname.title(), lastname.title())
    ...
    >>> result = get_hello('terry', 'gilliam')
    >>> result
    'Hello Terry Gilliam'

    >>> result2 = say_hello('terry', 'gilliam')
    Hello Terry Gilliam
    >>> result2
    >>>

### Les paramètres d’une fonction

Il existe 3 types de paramètre.

#### Les paramètres explicites

- Les paramètres explicites sont séparés par une virgule et peuvent être enrichis d’une valeur par défaut, ils sont dans ce cas optionnels.

- Il est nécessaire de regrouper les paramètres optionnels à la fin de la liste des paramètres.

- Les paramètres peuvent être nommés sans respecter un ordre précis.

    >>> def say_hello(firstname, gender='Mr', lastname=''):
    ...     fullname = ' '.join([gender, firstname, lastname])
    ...     print("Hello {}".format(fullname))
    ...
    >>> say_hello('Terry')
    Hello Mr Terry
    >>> say_hello('Yolande', 'Mme')
    Hello Mme Yolande
    >>> say_hello('Yolande', lastname='Moreau', gender='Mme')
    Hello Mme Yolande Moreau

#### Les paramètres non explicites

- Les paramètres non explicites permettent de spécifier autant de valeurs que l’on souhaite sans qu’il soit nécessaire de les spécifier dans la signature de la fonction.

- Ils sont fournis sous la forme nom=valeur, et sont accessibles à l’interieur de la fonction sous forme de dictionnaire.

- Les paramètres non explicites sont préfixés d’une double étoile.

    >>> def show_actors(**actors):
    ...     for actor, name in actors.items():
    ...         print("{}:{}" .format(actor, name))
    ...
    >>> show_actors(actor1='Terry Gilliam', actor2='John Cleese')
    actor2:John Cleese
    actor1:Terry Gilliam

#### Les paramètres arbitraires

- Les paramètres arbitraires fonctionnent de la même manière que les paramètres non explicites mais ne sont pas nommés.

- Il sont accessibles à l’intérieur de la fonction sous forme d’un tuple.

- Les paramètres arbitraires sont préfixés d’une étoile.

      >>> def ajoute_acteurs(*actors):
      ...     liste = []
      ...     for actor in actors:
      ...         liste.append(actor)
      ...     return liste
      ...
      >>> ajoute_cteurs('Terry Gilliam', 'John Cleese')
      ['John Cleese', 'Terry Gilliam']

Ces trois types de paramètres peuvent être utilisés dans une même fonction, dans ce cas leur ordre doit être le suivant:

def fonction(param1, param2=2, *arbitraires, **non-explicites):

### Les docstrings

Les docstrings sont des chaines de caractères placées au début du corps des fonctions. Ils sont associés à la variable __doc__ de la fonction.

    >>> def get_full_name(firstname, lastname):
    ...     """Retourne le nom complet"""
    ...     return '{} {}'.format(firstname, lastname)
    ...
    >>> get_full_name.__doc__
    'Retourne le nom complet'
    >>> help(get_full_name)
    'Retourne le nom complet'

### Les typages

Avec Python 3.5, il est possible de déterminer le type des entrées/sorties de vos fonctions. Il suffit de respecter les règles décrites ici :

https://docs.python.org/3/library/typing.html

## Les classes

Une classe est définie par le mot clé class.

Exemple de classe

    >>> class Human:
    ...     """Un humain"""
    ...     firstname = ''
    ...     lastname = ''

L’instanciation d’une classe s’effectue en appelant la classe.

    >>> humain1 = Human()
    >>> humain2 = Human()

Ces deux instances sont issues d’une même classe mais sont deux instances différentes.

    >>> id(humain1)
    39754640
    >>> id(humain2)
    39754712

Accessibilité et modfication des attributs:

    >>> humain1.firstname
    ''
    >>> humain1.firstname='John'
    >>> humain1.firstname
    'John'

### Les méthodes

- Les méthodes de la classe ont toutes comme premier paramètre l’objet instancié, par convention ce paramètre s’appelle self.

- Le premier paramètre est fourni de manière transparente lors de l’appel de la méthode.

    >>> class Human:
    ...     """Un classe représentant un humain"""
    ...     firstname = ''
    ...     lastname = ''
    ...     def get_full_name(self):
    ...         """Retourne le nom complet"""
    ...         return '{} {}'.format(self.firstname, self.lastname)
    ...
    >>> human1 = Human()
    >>> human1.firstname = 'John'
    >>> human1.lastname = 'Cleese'
    >>> human1.get_full_name()
    'John Cleese'

### L’héritage

Python permet de définir l’héritage de classe de façon fort simple.

    >>> class Actor(Human):
    ...     """Un acteur"""
    ...
    >>> actor1=Actor()
    >>> dir(actor1)
    ['__doc__', '__module__', 'firstname', 'get_full_name', 'lastname']

L’héritage multiple est supporté par python, mais en pratique il est fortement déconseillé.

### Constructeur

Lors de l’instanciation d’une classe la méthode spéciale __init__ est invoquée et prend en paramètre l’objet instancié par l’interpréteur. Cette méthode permet l’initialisation des attributs lors de l’instanciation d’une classe.

    >>> class Actor(Human):
    ...     """Un acteur"""
    ...     def __init__(self, firstname, lastname):
    ...         self.firstname = firstname
    ...         self.lastname = lastname
    ...
    >>> actor = Actor('John', 'Cleese')
    >>> actor.get_full_name()
    'John Cleese'

### Propriétés privées

Il est possible de définir des attributs privés en les préfixant d’un double “_”. L’attribut n’est alors pas accessible directement.

    >>> class Actor(Human):
    ...     """Un acteur"""
    ...     def __init__(self, firstname, lastname, gender):
    ...         self.firstname = firstname
    ...         self.lastname = lastname
    ...         self.__gender = gender
    ...
    >>> actor1 = Actor('John', 'Cleese', 'Mr')
    >>> actor1.firstname
    'John'
    >>> actor1.gender
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    AttributeError: Actor instance has no attribute 'gender'

Mais cette protection n’est pas absolue, il est possible d’accéder à l’attribut de cette manière

    >>> dir(actor1)
    ['_Actor__gender', '__doc__', '__init__', '__module__', 'firstname', 'get_full_name', 'lastname']
    >>> actor1._Actor__gender
    'Mr'

## Créer un projet Python

Cookie cutter est votre ami !

D’abord, il faut l’installer:

https://cookiecutter.readthedocs.io/en/2.0.2/installation.html

Puis, à partir d’un template: - pour une logique « github » (CI/CD) : https://cookiecutter-python.readthedocs.io/en/latest/tutorial.html - pour une logique « package local » : https://dev.to/chaps/creating-a-python-package-with-cookiecutter-3ik0

## Exercice

Création d’un jeu du pendu : 1 classe « JeuPendu », variables : nb_essais, mots, historique, méthode start() pour lancer l’application. Interaction via la console (fonction input(« XXX »)) Trame: script tests_pendu.py