簡體   English   中英

Python 選擇將 integer 除法向負無窮大的數學原因是什么?

[英]What's the mathematical reason behind Python choosing to round integer division toward negative infinity?

我知道 Python //向負無窮大舍入,在 C++ /中截斷,向 0 舍入。

到目前為止,這是我所知道的:

               |remainder| 
-12 / 10  = -1,   - 2      // c++
-12 // 10 = -2,   + 8      # python

12 / -10  = -1,     2      // c++
12 // -10 = -2,   - 8      # python

12 / 10  = 1,      2       //both
12 // 10 = 1,      2 

-12 / -10 = 1,    - 2      //both
          = 2,    + 8

C++: 
1. m%(-n) == m%n
2. -m%n == -(m%n)
3. (m/n)*n + m%n == m

python:
1. m%(-n) == -8 == -(-m%n)
2. (m//n)*n + m%n == m

但是為什么 Python //選擇向負無窮大取整? 我沒有找到任何資源解釋這一點,但只是找到並聽到人們含糊地說: “出於數學原因”

例如, 為什么在 C++ 中 -1/2 被評估為 0,但在 Python 中被評估為 -1?

抽象地處理這些事情的人傾向於覺得向負無窮大更有意義(這意味着它與數學中定義的模 function 兼容,而不是 % 有一些有趣的含義)。

但我沒有看到 C++ 的/與模 function 不兼容。 在 C++ 中, (m/n)*n + m%n == m也適用。

那么 Python 選擇舍入到負無窮大的(數學)原因是什么?


我想我應該把這個這是 Guido van Rossum 的舊博客文章放在這個主題上。 在問題正文中,以防人們錯過評論。

但是為什么 Python //選擇向負無窮大取整?

我不確定最初做出這個選擇原因是否在任何地方都有記錄(盡管據我所知,它可以在某處的一些 PIP 中詳細解釋),但我們當然可以想出它為什么會產生的各種原因感覺。

一個原因很簡單,向負(或正)無窮大舍入意味着所有數字都以相同的方式四舍五入。 而向零舍入會使零變得特殊。 數學上的說法是,向下舍入到 -∞ 是平移不變的,即它滿足方程:

round_down(x + k) == round_down(x) + k

對於所有實數x和所有整數k 向零舍入不會,因為例如:

round_to_zero(0.5 - 1).= round_to_zero(0.5) - 1

當然,其他 arguments 也存在,例如您引用的基於與%運算符(行為)的兼容性(我們希望如何)的參數 - 更多內容如下。

實際上,我想說的真正問題是為什么 Python 的int() function沒有定義為將浮點 arguments 舍入為負無窮大,因此m // n將等於int(m / n) (我懷疑“歷史原因”。)再說一次,這沒什么大不了的,因為 Python 至少有math.floor()滿足m // n == math.floor(m / n)


但我沒有看到 C++ 的/與模 function 不兼容。 在 C++ 中, (m/n)*n + m%n == m也適用。

沒錯,但是在將/舍入為零的同時保持該身份需要以一種尷尬的方式為負數定義% 特別是,我們失去了 Python 的以下兩個有用的數學屬性%

  1. 0 <= m % n < abs(n)對於所有mn
  2. (m + k * n) % n == m % n對於所有整數mnk

這些屬性很有用,因為%的主要用途之一是“環繞”數字m到有限的長度范圍n


例如,假設我們正在嘗試計算方向:假設heading是我們當前的羅盤航向(以度為單位)(從正北順時針計數, 0 <= heading < 360 )並且我們想在angle后計算新航向度(如果我們順時針轉動angle > 0 ,或者如果我們逆時針轉動angle < 0 )。 使用 Python 的%運算符,我們可以簡單地計算我們的新標題:

heading = (heading + angle) % 360

這將在所有情況下都有效。

但是,如果我們嘗試在 C++ 中使用這個公式,它的舍入規則和相應的%運算符不同,我們會發現環繞並不總是按預期工作,例如,如果我們開始面向西北 ( heading = 315 ) 並順時針旋轉 90° ( angle = 90 ),我們最終將面向東北 ( heading = 45 )。 但是,如果然后嘗試逆時針轉回90°( angle = -90 ),使用 C++ 的%運算符,我們將不會像預期的那樣回到heading = 315 ,而是在heading = -45

要使用 C++ %運算符獲得正確的環繞行為,我們需要將公式編寫為:

heading = (heading + angle) % 360;
if (heading < 0) heading += 360;

或作為:

heading = ((heading + angle) % 360) + 360) % 360;

(更簡單的公式heading = (heading + angle + 360) % 360只有在我們始終能夠保證heading + angle >= -360時才有效。)

這是您為除法的非平移不變舍入規則以及非平移不變的%運算符所付出的代價。

Python 的 a // b = floor(a/b) 采用標准 (ASCII) 數學符號。 (在德國,高斯符號 [x] 常見於 floor(x)。)地板 function 非常流行(經常使用⇔有用;谷歌查看數百萬示例)。 首先可能是因為它簡單自然:“最大的 integer ≤ x”。 因此,它具有許多不錯的數學特性,例如:

  • 由 integer k 翻譯: floor(x + k) = floor(x) + k。
  • 歐幾里得除法:x = y · q + r 0 ≤ r < q:= floor(x/y) 對於給定的 x 和 y。

我能想到的“向零舍入”function 的任何定義都會更加“人為”,並且涉及 if-then (可能隱藏在絕對值 |.| 或類似值中)。 我不知道有任何數學書介紹了“向零舍入”function。 這已經是采用此約定而不是其他約定的充分理由。

我不會在其他答案中詳述“與模運算的兼容性”論點,但必須提到它,因為它當然也是一個有效的論點,並且它與上述“翻譯”公式相關聯。 例如,在三角函數中,當您需要模 2 π 的角度時,您肯定需要這種除法。

但是為什么 Python //選擇向負無窮大取整?

根據python-history.blogspot.com Guido van Rossum選擇了這樣的行為//因為

(...)有一個很好的數學原因。 integer 除法運算 (//) 及其兄弟,模運算 (%),go 一起滿足良好的數學關系(所有變量都是整數):

a/b = q 余數 r

這樣

b*q + r = a 和 0 <= r < b

(假設 a 和 b >= 0)。

如果您希望關系擴展為負 a(保持 b 為正),您有兩種選擇:如果您將 q 截斷為零,r 將變為負數,因此不變量變為 0 <= abs(r) < 否則,您可以將 q 設為負無窮大,並且不變量保持 0 <= r < b(...) 在數學數論中,數學家總是更喜歡后一種選擇 (...)。 對於 Python,我做出了同樣的選擇,因為模運算有一些有趣的應用,其中 a 的符號是無趣的。 考慮采用 POSIX 時間戳(自 1970 年初以來的秒數)並將其轉換為一天中的時間。 因為一天有 24*3600 = 86400 秒,所以這個計算只是 t % 86400。但是如果我們用負數來表示 1970 年之前的時間,“向零截斷”規則將給出毫無意義的結果。 使用底線規則一切正常。 我想到的其他應用是計算機圖形中像素位置的計算。 我敢肯定還有更多。

所以總結一下//行為選擇是因為它與%行為保持一致,選擇后者是因為它在處理負(1970 年開始之前)時間戳和像素方面很有用。

來自 python 文檔( https://docs.python.org/2/reference/expressions.ZFC35FDC70D52C79D269EZ883A8 ):

/(除法)和 //(下除法)運算符產生其 arguments 的商。 數字 arguments 首先轉換為普通類型。 普通或長 integer 划分產生相同類型的 integer; 結果是數學除法,將“地板”function 應用於結果。

注意在最后一行它說floor function 應用於結果。

So let's see the behaviour of the floor function in python ( https://docs.python.org/3/library/math.html ):

math.floor(x) 返回 x 的下限,最大 integer 小於等於 x。 如果 x 不是浮點數,則委托給 x。 floor (),它應該返回一個 Integral 值。

here the logic of the math.floor(x) function in python is to get the largest integer less than or equal to x and it's the same logic with the floor() function in c++ ( https://www.cplusplus.com/參考/cmath/floor/ )。

But because the division operator in c++ depends on the operands datatypes so it just truncates the fractional part and yields only the integer part of the quotient of the division in case the two operands were integers ( https://www.informit.com/articles /article.aspx?p=352857&seqNum=4 ):

除法轉移:您還沒有看到有關除法運算符(/)的故事的rest。 此運算符的行為取決於操作數的類型。 如果兩個操作數都是整數,則 C++ 執行 integer 除法。 這意味着答案的任何小數部分都將被丟棄,從而使結果為 integer。 如果一個或兩個操作數都是浮點值,則保留小數部分,使結果為浮點數。

因此,在兩種語言中,地板的邏輯是相同的,並且是向負無窮大舍入,但是對於 python,地板除法實際上調用地板 function,這與 c++ 處理兩個整數時的除法行為不同。

為什么floor向負無窮大舍入是因為它是獲得小於或等於所應用的數 floor 的最大 integer 的最合理方法。

以數字-6.8為例,對其應用下限 function 結果將是-7現在-7是最大的 integer 小於或等於-6.8因為我們向負無窮大取整,但是如果我們只截斷小數部分怎么辦的號碼?

它將是-6這實際上比我們原來的數字-6.8大,所以我認為這取決於你是要截斷小數部分還是要得到除法商的底數。

盡管我無法提供關於為什么/如何選擇舍入模式的正式定義,但是當您認為%完全相同時,關於與%運算符兼容性的引用確實是有意義的。在 C++ 和 Python 中。

在 C++ 中,它是余數運算符,而在 Python 中,它是模數運算符 - 當兩個操作數有不同的符號時,它們不一定是相同的東西。 在以下答案中對這些運算符之間的區別有一些很好的解釋: “mod”和“remainder”有什么區別?

現在,考慮到這些差異,integer 除法的舍入(截斷)模式必須與兩種語言中的一樣,以確保在有quotient = dividend / divisormod_or_rem = quotient % divisor之類的表達式時,精確的原始被除數的值由表達式恢復, restored = quotient * divisor + mod_or_rem

這里有兩個簡短的程序演示了這一點(請原諒我有點天真的 Python 代碼——我是該語言的初學者):

C++:

#include <iostream>

int main()
{
    int dividend, divisor, quotient, remainder, check;
    std::cout << "Enter Dividend: ";                        // -27
    std::cin >> dividend;
    std::cout << "Enter Divisor: ";                         // 4
    std::cin >> divisor;

    quotient = dividend / divisor;
    std::cout << "Quotient = " << quotient << std::endl;    // -6
    remainder = dividend % divisor;
    std::cout << "Remainder = " << remainder << std::endl;  // -3

    check = quotient * divisor + remainder;
    std::cout << "Check = " << check << std::endl;          // 27
    return 0;
}

Python:

print("Enter Dividend: ")             # -27
dividend = int(input())
print("Enter Divisor: ")              # 4
divisor = int(input())
quotient = dividend // divisor;
print("Quotient = " + str(quotient))  # -7
modulus = dividend % divisor;
print("Remainder = " + str(modulus))  # 1
check = quotient * divisor + modulus; # -27
print("Check = " + str(check))

請注意,對於不同符號(-27 和 4)的給定輸入,商和余數/模在兩種語言之間都是不同的,而且恢復的check值在兩種情況下都是正確的

整數和實數算術都定義了它們的除法運算符,以便以下兩個等式適用於所有 n 和 d 值。

(n+d)/d = (n/d)+1
(-n)/d = -(n/d)

不幸的是,integer 算術不能以兩者都成立的方式定義。 出於許多目的,第一個等價比第二個更有用,但在代碼將兩個值相除的大多數情況下,以下之一將適用:

  1. 這兩個值都是正的,在這種情況下,第二個等價是無關緊要的。

  2. 被除數是除數的精確 integer 倍數,在這種情況下,兩個等式可以同時成立。

從歷史上看,處理涉及負數的除法的最簡單方法是觀察是否恰好一個操作數為負,刪除符號,執行加法,然后如果恰好有一個操作數為負,則將結果設為負數。 這將在兩種常見情況下都按要求運行,並且比使用在所有情況下都支持第一個等價的方法便宜,而不是僅當除數是股息的精確倍數時。

Python 不應被視為使用劣等語義,而是決定在重要的情況下通常優越的語義即使在精確的語義無關緊要的情況下也值得稍微減慢除法。

“出於數學原因”

考慮一個問題(在視頻游戲中很常見),您的 X 坐標可能為負數,並希望將其轉換為平鋪坐標(假設 16x16 平鋪)和平鋪內的偏移

Python 的%直接給你偏移量,而/直接給你平鋪:

>>> -35 // 16 # If we move 35 pixels left of the origin...
-3
>>> -35 % 16 # then we are 13 pixels from the left edge of a tile in row -3.
13

(而且divmod一次給你兩個。)

在這里,我為 integer 除法運算符編寫div ,為余數運算符編寫mod

divmod必須定義為,對於ab非零b整數,我們有

a == (a div b)*b + (a mod b)

通常,您希望mod結果始終介於 0(包括)和b (不包括)之間,而不管a的符號如何。 例如, a可以是循環緩沖區的索引, b可以是其(正)大小。 然后a mod b可以用作底層 memory 數組的索引,即使a是負數。 事實上,使用a == -1來引用最后一個緩沖區元素是非常流行的。

這可能是 Python 將商舍入負無窮大的原因。 因此,余數要么為零,要么具有除數的符號,而與被除數的符號無關。 這是一個基於字母的 plot 用於固定除數:

   ^ y = x mod 5
 4 |   *    *    *    *    *
   |  *    *    *    *    *
   | *    *    *    *    *    *
   |*    *    *    *    *    *
 0 +----*----*----*----*----*->
       -5    0    5   10   15 x

在 C/C++ 中,由於整數的寬度有限,事情變得有點復雜。

假設a == INT_MIN ,在二進制補碼表示中是 2 的某個負冪,並且b == 3 如果我們將商四舍五入使得a mod b > 0 ,那么(a div b)*b必須小於INT_MIN ,這將構成有符號整數溢出。 然后效果將由實現定義。 機器甚至會中斷執行,例如在使用 GCC 的-ftrapv選項進行編譯時。 但是,具體化 integer 除法和余數運算的行為的基本原理是擺脫這種不確定性。

因此,留給 C/C++ 的唯一選擇是將商數舍入為零。 因此,余數,如果非零,則具有被除數的符號。

缺點是固定除數的 plot 看起來不太規則:

   ^ y = x mod 5
 4 |             *    *    *
   |            *    *    *
   |           *    *    *    *
   |          *    *    *    *
 0 |    *    *    *    *    *
   |   *    *
   |  *    *
   | *    *
-4 |*    *
   +----+----+----+----+----+->
       -5    0    5   10   15 x

因此, mod buffer-size不會像我們希望的那樣處理負索引值。 編程方面,我不喜歡這個決定,盡管即使在極端情況下我也能理解實現a == (a div b)*b + (a mod b)的理由。

Python 選擇將 integer 除法舍入為負無窮大的數學原因是它是數學上最一致的選項。 在 Python 中,當您將兩個整數相除時,結果將始終是浮點數。 該數字將四舍五入到最接近的 integer,正數向上舍入,負數向下舍入。 這種一致的舍入行為導致向負無窮行為舍入。

Python 將 integer 除法向負無窮大的數學原因是,它比向正無窮大的四舍五入給出更一致的結果。 例如,考慮以下兩個表達式:

3 / 4

-3 / 4

第一個表達式將產生值 0.75,而第二個表達式將產生值 -0.75。 這是因為第一個表達式向正無窮大舍入,而第二個表達式向負無窮大舍入。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM