web-dev-qa-db-fra.com

Pourquoi la division entière est-elle arrondie dans de nombreux langages de script?

Dans les langues que j'ai testées, - (x div y ) n'est pas égal à -x div y; J'ai testé // en Python, / en Ruby, div en Perl 6; C a un comportement similaire .

Ce comportement est généralement conforme aux spécifications, car div est généralement défini comme l’arrondi par le bas du résultat de la division , cependant cela n’a pas beaucoup de sens du point de vue arithmétique, car cela fait que div se comporte différemment selon le signe, et que cela provoque une confusion telle que this poste sur comment cela se fait en Python .

Existe-t-il une raison spécifique à cette décision de conception ou bien est-ce que div est défini de cette façon à partir de zéro? Apparemment Guido van Rossum utilise un argument de cohérence dans un article de blog qui explique comment cela se fait en Python, mais vous pouvez aussi avoir une cohérence si vous choisissez d'arrondir.

(Inspiré par cette question posée par PMurias dans le canal # Perl6 IRC) )

42
jjmerelo

Les opérations à virgule flottante sont définies par IEEE754 en gardant à l’esprit les applications numériques et, par défaut, arrondies à la valeur représentable la plus proche de manière très stricte.

Les opérations entières sur les ordinateurs ne sont pas définies par les normes internationales générales. Les opérations accordées par les langues (en particulier celles de la famille C) ont tendance à suivre tout ce que l’ordinateur sous-jacent fournit. Certaines langues définissent certaines opérations de manière plus robuste que d'autres, mais pour éviter des implémentations excessivement difficiles ou lentes sur les ordinateurs disponibles (et populaires) de leur époque, vous choisirez une définition qui suit de près son comportement.

Pour cette raison, les opérations sur les nombres entiers ont tendance à boucler lors du débordement (pour addition, multiplication et décalage à gauche), et arrondit vers l'infini négatif lors de la production d'un résultat inexact (pour la division et le décalage à droite). Les deux sont simples troncatures à leur extrémité respective de l'entier en arithmétique binaire à complément à deux; le moyen le plus simple de gérer un cas d'angle.

D'autres réponses traitent de la relation avec le reste ou l'opérateur de module qu'un langage pourrait fournir parallèlement à la division. Malheureusement, ils l'ont à l'envers. Le reste dépend de la définition de la division, et non l'inverse, alors que le module peut être défini indépendamment de la division - si les deux arguments sont positifs et que la division est arrondie, ils sont identiques, alors les gens remarquent rarement.

La plupart des langages modernes fournissent soit un opérateur de reste, soit un opérateur de module, rarement les deux. Une fonction de bibliothèque peut constituer l’autre opération pour les personnes soucieuses de la différence, c’est-à-dire que le reste conserve le signe du dividende, tandis que le module conserve le signe du diviseur.

37
Chromatix

Idéalement, nous aimerions avoir deux opérations div et mod satisfaisantes pour chaque b>0:

  1. (a div b) * b + (a mod b) = a
  2. 0 <= (a mod b) < b
  3. (-a) div b = -(a div b)

C'est cependant une impossibilité mathématique. Si tout ce qui précède était vrai, nous aurions

1 div 2 = 0
1 mod 2 = 1

puisqu'il s'agit de la solution unique unique à (1) et (2). Par conséquent, nous aurions aussi, par (3),

0 = -0 = -(1 div 2) = (-1) div 2

qui, par (1), implique

-1 = ((-1) div 2) * 2 + ((-1) mod 2) = 0 * 2 + ((-1) mod 2) = (-1) mod 2

rendant (-1) mod 2 < 0 qui contredit (2).

Par conséquent, nous devons abandonner certaines propriétés parmi (1), (2) et (3).

Certains langages de programmation abandonnent (3) et font div arrondir vers le bas (Python, Ruby).

Dans certains cas (rares), le langage propose plusieurs opérateurs de division. Par exemple, en Haskell, nous avons div,mod Ne satisfaisant que (1) et (2), comme Python, et nous avons aussi quot,rem Ne satisfaisant que (1) et (3). La dernière paire d'opérateurs arrondit la division vers zéro, au prix de restituer les restes négatifs, par exemple, nous avons (-1) `quot` 2 = 0 Et (-1) `rem` 2 = (-1).

C # abandonne également (2) et permet à % De renvoyer un reste négatif. De manière cohérente, la division entière arrondit vers zéro. Java, Scala, Pascal et C, à partir de C99, adoptent également cette stratégie.

44
chi

Wikipedia a un excellent article à ce sujet , incluant l’histoire ainsi que la théorie.


Tant qu'une langue satisfait la propriété de division euclidienne que (a/b) * b + (a%b) == a, La division de plancher et la division de troncature sont cohérentes et arithmétiquement raisonnables.


Bien sûr, les gens aiment affirmer que l’une est manifestement correcte et que l’autre est manifestement fausse, mais elle a plus le caractère d’une guerre sainte qu’une discussion sensée, et elle a généralement plus à voir avec le choix de la première langue qu’ils ont préférée autre. Ils ont aussi souvent tendance à argumenter principalement en faveur de leur choix %, Même s'il est probablement plus logique de choisir d'abord /, Puis de choisir le % Correspondant.

  • Revêtement de sol (comme Python):
    • Pas moins une autorité que ne le suggère Donald Knuth.
    • % Après le signe du diviseur est apparemment ce qu'environ 70% des étudiants devinent
    • L'opérateur est généralement lu sous la forme mod ou modulo plutôt que remainder.
    • "C le fait" - ce qui n'est même pas vrai.1
  • Troncature (comme C++):
    • Rend la division entière plus cohérente avec la division float IEEE (en mode d'arrondi par défaut).
    • Plus de processeurs l'implémentent. (Peut ne pas être vrai à différents moments de l'histoire.)
    • L'opérateur est lu modulo plutôt que remainder (même si cela plaide réellement contre leur point).
    • Sur le plan conceptuel, la propriété de division concerne davantage le reste que le module.
    • L'opérateur est lu mod plutôt que modulo, il devrait donc suivre la distinction de Fortran. (Cela peut sembler idiot, mais peut avoir été le facteur décisif pour C99. Voir ce fil .)
  • "Euclidien" (comme Pascal - / Pose ou tronque selon les signes, ainsi % N'est jamais négatif):
    • Niklaus Wirth a affirmé que personne n'est jamais surpris par un mod positif.
    • Raymond T. Boute a plus tard fait valoir que vous ne pouvez pas appliquer naïvement la division euclidienne avec l'une ou l'autre des règles.

Un certain nombre de langues fournissent les deux. Typiquement, comme dans Ada, Modula-2, Lisps, Haskell et Julia, ils utilisent des noms associés à mod pour l'opérateur de style Python et à rem pour l'opérateur de style C++. Mais pas toujours - Fortran, par exemple, appelle les mêmes choses modulo et mod (comme mentionné ci-dessus pour C99).


Nous ne savons pas pourquoi Python, Tcl, Perl et les autres langages de script influents ont principalement choisi les revêtements de sol. Comme indiqué dans la question, la réponse de Guido van Rossum explique uniquement pourquoi il a dû choisir l'une des trois réponses cohérentes, pas pourquoi il a choisi celle qu'il a choisie.

Cependant, je soupçonne que l’influence de C était la clé. La plupart des langages de script sont implémentés (du moins initialement) en C et empruntent leur inventaire d'opérateur à C. Le code d'implémentation défini par C89 % Est manifestement cassé et ne convient pas à un langage "convivial" comme Tcl ou Python. Et C appelle l'opérateur "mod". Alors ils vont avec module, pas reste.


1. En dépit de ce que dit la question - et de nombreuses personnes l’utilisant comme argument -, C n’a pas un comportement similaire à Python et ses amis. C99 nécessite une division tronquée et non pas un plancher. C89 est autorisé non plus, mais aussi l'une ou l'autre version du mod, il n'y a donc aucune garantie quant à la propriété de la division, ni aucun moyen d'écrire du code portable pour la division d'entiers signés. cassé.

12
abarnert

En raison de la division entière, la réponse complète inclut un reste.

12
Paula Thomas

Comme Paula l'a dit, c'est à cause du reste.

L'algorithme est fondé sur division euclidienne .

En Ruby, vous pouvez écrire cette reconstruction du dividende avec cohérence:

puts (10/3)*3 + 10%3
#=> 10

Cela fonctionne de la même manière dans la vie réelle. 10 pommes et 3 personnes. Ok vous pouvez en couper un Apple en trois, mais en dehors des entiers définis.

Avec les nombres négatifs, la cohérence est également conservée:

puts (-10/3)*3 + -10%3 #=> -10
puts (10/(-3))*(-3) + 10%(-3) #=> 10
puts (-10/(-3))*(-3) + -10%(-3) #=> -10

Le quotient est toujours arrondi vers le bas (le long de l'axe négatif) et le rappel suit:

puts (-10/3) #=> -4
puts -10%3 #=> 2

puts (10/(-3)) #=> -4
puts 10%(-3) # => -2

puts (-10/(-3)) #=> 3
puts -10%(-3) #=> -1 
7
iGian