tampon circulaire efficace?


109

Je souhaite créer un tampon circulaire efficace en python (dans le but de prendre des moyennes des valeurs entières dans le tampon).

Est-ce un moyen efficace d'utiliser une liste pour collecter des valeurs?

def add_to_buffer( self, num ):
    self.mylist.pop( 0 )
    self.mylist.append( num )

Qu'est-ce qui serait plus efficace (et pourquoi)?


Ce n'est pas un moyen efficace d'implémenter un tampon circulaire car pop (0) est l'opération O (n) dans la liste. pop (0) supprime le premier élément de la liste et tous les éléments doivent être décalés vers la gauche. Utilisez plutôt collections.deque avec l'attribut maxlen. deque a une opération O (1) pour ajouter et pop.
Vlad Bezden le

Réponses:


205

J'utiliserais collections.dequeavec un maxlenarg

>>> import collections
>>> d = collections.deque(maxlen=10)
>>> d
deque([], maxlen=10)
>>> for i in xrange(20):
...     d.append(i)
... 
>>> d
deque([10, 11, 12, 13, 14, 15, 16, 17, 18, 19], maxlen=10)

Il y a une recette dans la documentation pour dequequi est similaire à ce que vous voulez. Mon affirmation selon laquelle c'est le plus efficace repose entièrement sur le fait qu'il est implémenté en C par une équipe incroyablement qualifiée qui a l'habitude de lancer un code de premier ordre.


7
+1 Oui, ce sont les belles piles incluses. Les opérations pour le tampon circulaire sont O (1) et comme vous le dites, la surcharge supplémentaire est en C, elle devrait donc être assez rapide
John La Rooy

7
Je n'aime pas cette solution car la documentation ne garantit pas l'accès aléatoire O (1) lorsque maxlenest défini. O (n) est compréhensible lorsque le dequepeut atteindre l'infini, mais s'il maxlenest donné, l'indexation d'un élément doit être en temps constant.
lvella

1
Je suppose que son implémentation est une liste chaînée et non un tableau.
e-satis

1
Cela semble à peu près correct, si les horaires de ma réponse ci - dessous sont corrects.
djvg

13

sortir de la tête d'une liste entraîne la copie de toute la liste, ce qui est inefficace

Vous devriez plutôt utiliser une liste / un tableau de taille fixe et un index qui se déplace dans la mémoire tampon lorsque vous ajoutez / supprimez des éléments


4
Se mettre d'accord. Peu importe à quel point il est élégant ou inélégant ou quelle que soit la langue utilisée. En réalité, moins vous dérangez le garbage collector (ou le gestionnaire de tas ou les mécanismes de pagination / mappage ou tout ce qui fait la magie de la mémoire), mieux c'est.

@RocketSurgeon Ce n'est pas magique, c'est juste que c'est un tableau dont le premier élément est supprimé. Donc, pour un tableau de taille n, cela signifie n-1 opérations de copie. Aucun garbage collector ou appareil similaire n'est impliqué ici.
Christian du

3
Je suis d'accord. Cela est également beaucoup plus facile que certaines personnes ne le pensent. Utilisez simplement un compteur toujours croissant et utilisez l'opérateur modulo (% arraylen) lors de l'accès à l'élément.
Andre Blum

idem, vous pouvez consulter mon post ci-dessus, c'est comme ça que je l'ai fait
MoonCactus

10

Basé sur la réponse de MoonCactus , voici une circularlistclasse. La différence avec sa version est qu'ici c[0]donnera toujours l'élément le plus ancien, c[-1]le dernier élément ajouté, c[-2]l'avant-dernier ... C'est plus naturel pour les applications.

c = circularlist(4)
c.append(1); print c, c[0], c[-1]    #[1]              1, 1
c.append(2); print c, c[0], c[-1]    #[1, 2]           1, 2
c.append(3); print c, c[0], c[-1]    #[1, 2, 3]        1, 3
c.append(8); print c, c[0], c[-1]    #[1, 2, 3, 8]     1, 8
c.append(10); print c, c[0], c[-1]   #[10, 2, 3, 8]    2, 10
c.append(11); print c, c[0], c[-1]   #[10, 11, 3, 8]   3, 11

Classe:

class circularlist(object):
    def __init__(self, size, data = []):
        """Initialization"""
        self.index = 0
        self.size = size
        self._data = list(data)[-size:]

    def append(self, value):
        """Append an element"""
        if len(self._data) == self.size:
            self._data[self.index] = value
        else:
            self._data.append(value)
        self.index = (self.index + 1) % self.size

    def __getitem__(self, key):
        """Get element by index, relative to the current index"""
        if len(self._data) == self.size:
            return(self._data[(key + self.index) % self.size])
        else:
            return(self._data[key])

    def __repr__(self):
        """Return string representation"""
        return self._data.__repr__() + ' (' + str(len(self._data))+' items)'

[Modifié]: Ajout d'un dataparamètre facultatif pour permettre l'initialisation à partir de listes existantes, par exemple:

circularlist(4, [1, 2, 3, 4, 5])      #  [2, 3, 4, 5] (4 items)
circularlist(4, set([1, 2, 3, 4, 5])) #  [2, 3, 4, 5] (4 items)
circularlist(4, (1, 2, 3, 4, 5))      #  [2, 3, 4, 5] (4 items)

Bon ajout. Les listes Python autorisent déjà les indices négatifs, mais (-1), par exemple, ne renverra pas la valeur attendue une fois que le tampon circulaire est plein, puisque le "dernier" ajout à la liste se termine dans la liste.
MoonCactus

1
Cela fonctionne @MoonCactus, voir les 6 exemples que j'ai donnés en plus de la réponse; dans les derniers, vous pouvez voir que c[-1]c'est toujours le bon élément. __getitem__le fait bien.
Basj

oh oui, je veux dire le mien a échoué, pas le vôtre, désolé: DI rendra mon commentaire plus clair! - oh je ne peux pas, le commentaire est trop vieux.
MoonCactus

belle solution simple. J'ai ajouté un argument optionnel pour permettre l'initialisation de la liste à partir de données existantes, c'est plus pythonpathétique de cette façon.
Orwellophile

9

Le deque de Python est lent. Vous pouvez également utiliser numpy.roll à la place. Comment faire pivoter les nombres dans un tableau numpy de forme (n,) ou (n, 1)?

Dans ce benchmark, deque est de 448ms. Numpy.roll est de 29 ms http://scimusing.wordpress.com/2013/10/25/ring-buffers-in-pythonnumpy/


1
Mais numpy.rollrenvoie une copie du tableau, non?
djvg

3
Cette réponse est très trompeuse - le deque de Python semble être assez rapide, mais la conversion de et vers des tableaux numpy le ralentit considérablement dans les benchmarks auxquels vous vous connectez.
xitrium du

7

ok avec l'utilisation de la classe deque, mais pour les requeriments de la question (moyenne) c'est ma solution:

>>> from collections import deque
>>> class CircularBuffer(deque):
...     def __init__(self, size=0):
...             super(CircularBuffer, self).__init__(maxlen=size)
...     @property
...     def average(self):  # TODO: Make type check for integer or floats
...             return sum(self)/len(self)
...
>>>
>>> cb = CircularBuffer(size=10)
>>> for i in range(20):
...     cb.append(i)
...     print "@%s, Average: %s" % (cb, cb.average)
...
@deque([0], maxlen=10), Average: 0
@deque([0, 1], maxlen=10), Average: 0
@deque([0, 1, 2], maxlen=10), Average: 1
@deque([0, 1, 2, 3], maxlen=10), Average: 1
@deque([0, 1, 2, 3, 4], maxlen=10), Average: 2
@deque([0, 1, 2, 3, 4, 5], maxlen=10), Average: 2
@deque([0, 1, 2, 3, 4, 5, 6], maxlen=10), Average: 3
@deque([0, 1, 2, 3, 4, 5, 6, 7], maxlen=10), Average: 3
@deque([0, 1, 2, 3, 4, 5, 6, 7, 8], maxlen=10), Average: 4
@deque([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], maxlen=10), Average: 4
@deque([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], maxlen=10), Average: 5
@deque([2, 3, 4, 5, 6, 7, 8, 9, 10, 11], maxlen=10), Average: 6
@deque([3, 4, 5, 6, 7, 8, 9, 10, 11, 12], maxlen=10), Average: 7
@deque([4, 5, 6, 7, 8, 9, 10, 11, 12, 13], maxlen=10), Average: 8
@deque([5, 6, 7, 8, 9, 10, 11, 12, 13, 14], maxlen=10), Average: 9
@deque([6, 7, 8, 9, 10, 11, 12, 13, 14, 15], maxlen=10), Average: 10
@deque([7, 8, 9, 10, 11, 12, 13, 14, 15, 16], maxlen=10), Average: 11
@deque([8, 9, 10, 11, 12, 13, 14, 15, 16, 17], maxlen=10), Average: 12
@deque([9, 10, 11, 12, 13, 14, 15, 16, 17, 18], maxlen=10), Average: 13
@deque([10, 11, 12, 13, 14, 15, 16, 17, 18, 19], maxlen=10), Average: 14

Je reçois TypeError: 'numpy.float64' object is not callableen essayant d'appeler la averageméthode
scls

Oui ... en fait, je suppose que deque utilise des tableaux numpy en interne (après avoir supprimé @property, cela fonctionne bien)
scls

17
Je garantis que deque n'utilise pas de tableaux numpy en interne. collectionsfait partie de la bibliothèque standard, numpyne l'est pas. Les dépendances sur des bibliothèques tierces feraient une terrible bibliothèque standard.

6

Bien qu'il existe déjà un grand nombre de bonnes réponses ici, je n'ai pas pu trouver de comparaison directe des horaires pour les options mentionnées. Par conséquent, veuillez trouver mon humble tentative de comparaison ci-dessous.

À des fins de test uniquement, la classe peut basculer entre un listtampon basé sur un tampon, un collections.dequetampon basé sur un tampon et un Numpy.rolltampon basé sur un tampon.

Notez que la updateméthode n'ajoute qu'une seule valeur à la fois, pour rester simple.

import numpy
import timeit
import collections


class CircularBuffer(object):
    buffer_methods = ('list', 'deque', 'roll')

    def __init__(self, buffer_size, buffer_method):
        self.content = None
        self.size = buffer_size
        self.method = buffer_method

    def update(self, scalar):
        if self.method == self.buffer_methods[0]:
            # Use list
            try:
                self.content.append(scalar)
                self.content.pop(0)
            except AttributeError:
                self.content = [0.] * self.size
        elif self.method == self.buffer_methods[1]:
            # Use collections.deque
            try:
                self.content.append(scalar)
            except AttributeError:
                self.content = collections.deque([0.] * self.size,
                                                 maxlen=self.size)
        elif self.method == self.buffer_methods[2]:
            # Use Numpy.roll
            try:
                self.content = numpy.roll(self.content, -1)
                self.content[-1] = scalar
            except IndexError:
                self.content = numpy.zeros(self.size, dtype=float)

# Testing and Timing
circular_buffer_size = 100
circular_buffers = [CircularBuffer(buffer_size=circular_buffer_size,
                                   buffer_method=method)
                    for method in CircularBuffer.buffer_methods]
timeit_iterations = 1e4
timeit_setup = 'from __main__ import circular_buffers'
timeit_results = []
for i, cb in enumerate(circular_buffers):
    # We add a convenient number of convenient values (see equality test below)
    code = '[circular_buffers[{}].update(float(j)) for j in range({})]'.format(
        i, circular_buffer_size)
    # Testing
    eval(code)
    buffer_content = [item for item in cb.content]
    assert buffer_content == range(circular_buffer_size)
    # Timing
    timeit_results.append(
        timeit.timeit(code, setup=timeit_setup, number=int(timeit_iterations)))
    print '{}: total {:.2f}s ({:.2f}ms per iteration)'.format(
        cb.method, timeit_results[-1],
        timeit_results[-1] / timeit_iterations * 1e3)

Sur mon système, cela donne:

list:  total 1.06s (0.11ms per iteration)
deque: total 0.87s (0.09ms per iteration)
roll:  total 6.27s (0.63ms per iteration)

4

Qu'en est-il de la solution du livre de recettes Python , y compris une reclassification de l'instance de tampon en anneau lorsqu'elle est pleine?

class RingBuffer:
    """ class that implements a not-yet-full buffer """
    def __init__(self,size_max):
        self.max = size_max
        self.data = []

    class __Full:
        """ class that implements a full buffer """
        def append(self, x):
            """ Append an element overwriting the oldest one. """
            self.data[self.cur] = x
            self.cur = (self.cur+1) % self.max
        def get(self):
            """ return list of elements in correct order """
            return self.data[self.cur:]+self.data[:self.cur]

    def append(self,x):
        """append an element at the end of the buffer"""
        self.data.append(x)
        if len(self.data) == self.max:
            self.cur = 0
            # Permanently change self's class from non-full to full
            self.__class__ = self.__Full

    def get(self):
        """ Return a list of elements from the oldest to the newest. """
        return self.data

# sample usage
if __name__=='__main__':
    x=RingBuffer(5)
    x.append(1); x.append(2); x.append(3); x.append(4)
    print(x.__class__, x.get())
    x.append(5)
    print(x.__class__, x.get())
    x.append(6)
    print(x.data, x.get())
    x.append(7); x.append(8); x.append(9); x.append(10)
    print(x.data, x.get())

Le choix de conception notable dans l'implémentation est que, étant donné que ces objets subissent une transition d'état non réversible à un moment de leur durée de vie - du tampon non complet au tampon complet (et des changements de comportement à ce stade) - j'ai modélisé cela en changeant self.__class__. Cela fonctionne même dans Python 2.2, tant que les deux classes ont les mêmes emplacements (par exemple, cela fonctionne bien pour deux classes classiques, telles que RingBuffer et __Fulldans cette recette).

Changer la classe d'une instance peut être étrange dans de nombreuses langues, mais c'est une alternative pythonique à d'autres façons de représenter des changements d'état occasionnels, massifs, irréversibles et discrets qui affectent considérablement le comportement, comme dans cette recette. Heureusement que Python le prend en charge pour toutes sortes de classes.

Crédit: Sébastien Keim


J'ai fait quelques tests de vitesse de ce vs deque. C'est environ 7 fois plus lent que deque.
PolyMesh

@PolyMesh génial, vous devriez le faire savoir à l'auteur!
d8aninja

1
quel serait l'intérêt de cela? C'est un ancien document publié. Le but de mon commentaire est de faire savoir aux autres que cette réponse est dépassée et d'utiliser deque à la place.
PolyMesh

@PolyMesh c'était probablement encore plus lent quand il l'a publié; les instructions pour contacter l'auteur se trouvent dans l'intro du livre. Je raconte juste une alternative possible. Aussi, "Si seulement la vitesse était la meilleure métrique; hélas, ce n'est peut-être qu'une bonne mesure."
d8aninja

3

Vous pouvez également voir cette recette Python assez ancienne .

Voici ma propre version avec le tableau NumPy:

#!/usr/bin/env python

import numpy as np

class RingBuffer(object):
    def __init__(self, size_max, default_value=0.0, dtype=float):
        """initialization"""
        self.size_max = size_max

        self._data = np.empty(size_max, dtype=dtype)
        self._data.fill(default_value)

        self.size = 0

    def append(self, value):
        """append an element"""
        self._data = np.roll(self._data, 1)
        self._data[0] = value 

        self.size += 1

        if self.size == self.size_max:
            self.__class__  = RingBufferFull

    def get_all(self):
        """return a list of elements from the oldest to the newest"""
        return(self._data)

    def get_partial(self):
        return(self.get_all()[0:self.size])

    def __getitem__(self, key):
        """get element"""
        return(self._data[key])

    def __repr__(self):
        """return string representation"""
        s = self._data.__repr__()
        s = s + '\t' + str(self.size)
        s = s + '\t' + self.get_all()[::-1].__repr__()
        s = s + '\t' + self.get_partial()[::-1].__repr__()
        return(s)

class RingBufferFull(RingBuffer):
    def append(self, value):
        """append an element when buffer is full"""
        self._data = np.roll(self._data, 1)
        self._data[0] = value

4
+1 pour utiliser numpy, mais -1 pour ne pas implémenter de tampon circulaire. La façon dont vous l'avez implémenté, vous déplacez toutes les données chaque fois que vous ajoutez un seul élément, cela prend du O(n)temps. Pour implémenter un tampon circulaire approprié , vous devez avoir à la fois un index et une variable de taille, et vous devez gérer correctement le cas où les données «enveloppent» la fin du tampon. Lors de la récupération de données, vous devrez peut-être concaténer deux sections au début et à la fin du tampon.
Bas Swinckels

2

Celui-ci ne nécessite aucune bibliothèque. Il agrandit une liste, puis cycle à l'intérieur par index.

L'encombrement est très petit (pas de bibliothèque), et il fonctionne au moins deux fois plus vite que la file d'attente. C'est bien pour calculer des moyennes mobiles, mais sachez que les éléments ne sont pas triés par âge comme ci-dessus.

class CircularBuffer(object):
    def __init__(self, size):
        """initialization"""
        self.index= 0
        self.size= size
        self._data = []

    def record(self, value):
        """append an element"""
        if len(self._data) == self.size:
            self._data[self.index]= value
        else:
            self._data.append(value)
        self.index= (self.index + 1) % self.size

    def __getitem__(self, key):
        """get element by index like a regular array"""
        return(self._data[key])

    def __repr__(self):
        """return string representation"""
        return self._data.__repr__() + ' (' + str(len(self._data))+' items)'

    def get_all(self):
        """return a list of all the elements"""
        return(self._data)

Pour obtenir la valeur moyenne, par exemple:

q= CircularBuffer(1000000);
for i in range(40000):
    q.record(i);
print "capacity=", q.size
print "stored=", len(q.get_all())
print "average=", sum(q.get_all()) / len(q.get_all())

Résulte en:

capacity= 1000000
stored= 40000
average= 19999

real 0m0.024s
user 0m0.020s
sys  0m0.000s

C'est environ 1/3 du temps de l'équivalent avec dequeue.


1
Ne devriez-vous pas __getitem__être un peu plus puissant self._data[(key + self._index + 1) % self._size]:?
Mateen Ulhaq

Pourquoi voudriez-vous changer de +1? Maintenant, oui, voir la variante Basj ci-dessous pour l'idée
MoonCactus

1

J'ai eu ce problème avant de faire de la programmation en série. À l'époque, il y a un peu plus d'un an, je ne trouvais pas non plus d'implémentations efficaces, j'ai donc fini par en écrire une en tant qu'extension C et elle est également disponible sur pypi sous une licence MIT. C'est super basique, ne gère que les tampons de caractères signés 8 bits, mais sa longueur est flexible, vous pouvez donc utiliser Struct ou quelque chose en plus si vous avez besoin d'autre chose que des caractères. Je vois maintenant avec une recherche Google qu'il existe plusieurs options ces jours-ci, vous voudrez peut-être les regarder aussi.


1

Votre réponse n'est pas juste. Le tampon circulaire principal a deux priciples (https://en.wikipedia.org/wiki/Circular_buffer )

  1. La durée du tampon est réglée;
  2. Premier entré, premier sorti;
  3. Lorsque vous ajoutez ou supprimez un élément, les autres éléments ne doivent pas bouger de leur position

votre code ci-dessous:

def add_to_buffer( self, num ):
    self.mylist.pop( 0 )
    self.mylist.append( num )

Considérons une situation où la liste est pleine, en utilisant votre code:

self.mylist = [1, 2, 3, 4, 5]

maintenant nous ajoutons 6, la liste est changée en

self.mylist = [2, 3, 4, 5, 6]

les éléments attendus 1 dans la liste ont changé de position

votre code est une file d'attente, pas un tampon circulaire.

La réponse de Basj, je pense, est la plus efficace.

À propos, un tampon circulaire peut améliorer les performances de l'opération d'ajout d'un élément.


1

Depuis Github:

class CircularBuffer:

    def __init__(self, size):
        """Store buffer in given storage."""
        self.buffer = [None]*size
        self.low = 0
        self.high = 0
        self.size = size
        self.count = 0

    def isEmpty(self):
        """Determines if buffer is empty."""
        return self.count == 0

    def isFull(self):
        """Determines if buffer is full."""
        return self.count == self.size

    def __len__(self):
        """Returns number of elements in buffer."""
        return self.count

    def add(self, value):
        """Adds value to buffer, overwrite as needed."""
        if self.isFull():
            self.low = (self.low+1) % self.size
        else:
            self.count += 1
        self.buffer[self.high] = value
        self.high = (self.high + 1) % self.size

    def remove(self):
        """Removes oldest value from non-empty buffer."""
        if self.count == 0:
            raise Exception ("Circular Buffer is empty");
        value = self.buffer[self.low]
        self.low = (self.low + 1) % self.size
        self.count -= 1
        return value

    def __iter__(self):
        """Return elements in the circular buffer in order using iterator."""
        idx = self.low
        num = self.count
        while num > 0:
            yield self.buffer[idx]
            idx = (idx + 1) % self.size
            num -= 1

    def __repr__(self):
        """String representation of circular buffer."""
        if self.isEmpty():
            return 'cb:[]'

        return 'cb:[' + ','.join(map(str,self)) + ']'

https://github.com/heineman/python-data-structures/blob/master/2.%20Ubiquitous%20Lists/circBuffer.py


0

La question initiale était: tampon circulaire " efficace ". D'après cette efficacité demandée, la réponse d'aaronasterling semble définitivement correcte. L'utilisation d'une classe dédiée programmée en Python et la comparaison du traitement du temps avec collections.deque montre une accélération x5,2 fois avec deque! Voici un code très simple pour tester ceci:

class cb:
    def __init__(self, size):
        self.b = [0]*size
        self.i = 0
        self.sz = size
    def append(self, v):
        self.b[self.i] = v
        self.i = (self.i + 1) % self.sz

b = cb(1000)
for i in range(10000):
    b.append(i)
# called 200 times, this lasts 1.097 second on my laptop

from collections import deque
b = deque( [], 1000 )
for i in range(10000):
    b.append(i)
# called 200 times, this lasts 0.211 second on my laptop

Pour transformer un deque en liste, il suffit d'utiliser:

my_list = [v for v in my_deque]

Vous obtiendrez alors un accès aléatoire O (1) aux objets deque. Bien sûr, cela n'a de valeur que si vous devez effectuer de nombreux accès aléatoires au deque après l'avoir défini une fois.


0

Cela applique le même principe à certains tampons destinés à contenir les messages texte les plus récents.

import time
import datetime
import sys, getopt

class textbffr(object):
    def __init__(self, size_max):
        #initialization
        self.posn_max = size_max-1
        self._data = [""]*(size_max)
        self.posn = self.posn_max

    def append(self, value):
        #append an element
        if self.posn == self.posn_max:
            self.posn = 0
            self._data[self.posn] = value   
        else:
            self.posn += 1
            self._data[self.posn] = value

    def __getitem__(self, key):
        #return stored element
        if (key + self.posn+1) > self.posn_max:
            return(self._data[key - (self.posn_max-self.posn)])
        else:
            return(self._data[key + self.posn+1])


def print_bffr(bffr,bffer_max): 
    for ind in range(0,bffer_max):
        stored = bffr[ind]
        if stored != "":
            print(stored)
    print ( '\n' )

def make_time_text(time_value):
    return(str(time_value.month).zfill(2) + str(time_value.day).zfill(2)
      + str(time_value.hour).zfill(2) +  str(time_value.minute).zfill(2)
      + str(time_value.second).zfill(2))


def main(argv):
    #Set things up 
    starttime = datetime.datetime.now()
    log_max = 5
    status_max = 7
    log_bffr = textbffr(log_max)
    status_bffr = textbffr(status_max)
    scan_count = 1

    #Main Loop
    # every 10 secounds write a line with the time and the scan count.
    while True: 

        time_text = make_time_text(datetime.datetime.now())
        #create next messages and store in buffers
        status_bffr.append(str(scan_count).zfill(6) + " :  Status is just fine at : " + time_text)
        log_bffr.append(str(scan_count).zfill(6) + " : " + time_text + " : Logging Text ")

        #print whole buffers so far
        print_bffr(log_bffr,log_max)
        print_bffr(status_bffr,status_max)

        time.sleep(2)
        scan_count += 1 

if __name__ == '__main__':
    main(sys.argv[1:])  

0

Vous pouvez extraire ce tampon circulaire basé sur un tableau numpy de taille prédéfinie. L'idée est que vous créez un tampon (allouez de la mémoire pour le tableau numpy) et que vous y ajoutez plus tard. L'insertion des données et la récupération sont très rapides. J'ai créé ce module dans un but similaire à celui dont vous avez besoin. Dans mon cas, j'ai un appareil qui génère des données entières. J'ai lu les données et les ai placées dans le tampon circulaire pour une analyse et un traitement futurs.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.