Stack Overflow 2016年度 20個最佳Python問題(一)

本文翻譯自20 best Python questions at stackoverflow in 2016(需翻牆)。

1.使用Pythonic的方式避免「if x: return x」這樣的語句

我依次調用4個方法檢查特定的條件,當其一返回真值時立刻返回。

def check_all_conditions():n x = check_size()n if x:n return xnn x = check_color()n if x:n return xnn x = check_tone()n if x:n return xnn x = check_flavor()n if x:n return xn return Nonen

以上看起來有些冗餘,與其使用兩行的if語句,我更願意這樣做:

x and return xn

但這在Python中是非法的,我是否錯失了一種簡單、優雅的解決方法?此外,在上述情況中,這四種檢查方法可能帶來開銷,所以我不想多次調用它們。

<回答>

1)使用循環

conditions = (check_size, check_color, check_tone, check_flavor)nfor condition in conditions:n result = condition()n if result:n return resultn

這還有額外的好處,能使條件數可變。

2)使用 map() + filter() (在Python2中使用 future_builtins versions 以獲得Python3版本) 獲得第一個匹配值

try:n # Python 2n from future_builtins import map, filternexcept ImportError:n # Python 3n passnnconditions = (check_size, check_color, check_tone, check_flavor)nreturn next(filter(None, map(lambda f: f(), conditions)), None)n

不過這種方式的可讀性是有爭議的。

3)使用生成器表達式

conditions = (check_size, check_color, check_tone, check_flavor)nchecks = (condition() for condition in conditions)nreturn next((check for check in checks if check), None)n

4)使用 or 連接,這會返回第一個真值或者None(如果沒有真值)

def check_all_conditions():n return check_size() or check_color() or check_tone() or check_flavor() or Nonen

Demo:

>>> x = [] or 0 or {} or -1 or Nonen>>> xn-1n>>> x = [] or 0 or {} or or Nonen>>> x is NonenTruen

2.如何理解Python循環中的「else」子句?

許多Python程序員可能不知道 while 循環和 for 循環的語法包括可選的 else 子句:

else 子句的主體是進行某些種類的清楚動作的好地方,並且在循環正常終止時執行:即,使用 return 或 break 退出循環時則跳過 else 子句;在 continue 後退出則執行它。我知道這隻在我想不起來何時執行 else 子句時會(再次)查閱它。

總是顧名思義般地在循環「失敗」時進入else?或在正常終止時進入?即使循環是以 return 結束?如果不查的話,我永遠不能完全確定。

在關鍵字一致性選擇上的不確定性讓我感到煩惱:我發現 else 這個語義簡直難以記憶。我的問題不是「為什麼是這個關鍵字用於這個目的」(雖然在只閱讀了答案和評論的情況下,我可能會投票停用它),而是,我怎樣思考 else 關鍵字以明白其語義,那樣我就能記住它了?

我相信有相當多關於這一點的討論。我也可以想像,為了一致性以及不添加Python保留字,選擇使用 try 語句和 else 子句(我肯定也要查閱)。也許選擇 else 的原因將使它的作用清晰、容易記憶。

<回答>

一個 if 語句在條件為假的時候運行其 else 語句。同樣的,一個 while 循環在條件為假的時候運行其 else 語句。

這條規則匹配了你描述的規則:

  • 在正常執行中, while 循環重複運行直至條件為假,因此很自然的退出循環並進入 else 子句。
  • 當執行 break 語句時,會不經條件判斷直接退出循環,所以條件就不能為假,也就永遠不會執行 else 子句。
  • 當執行 continue 語句時,會再次進行條件判斷,然後在循環迭代的開始處正常執行。所以如果條件為真,就接著循環,如果條件為假就運行 else 子句。
  • 其他退出循環的方法,比如說 return ,不會經過條件判斷,所以就不會運行 else 子句。

for 循環也是一個道理。就是去考慮如果迭代器中還有元素,條件就為真,反之亦然。

3.如何避免 __init__中 「self.x = x; self.y = y; self.z = z」 這樣的模式?

def __init__(self, x, y, z):n ...n self.x = xn self.y = yn self.z = zn ...n

上述這種模式很常見,經常有很多參數。是否有一種好的方式,能否避免這種重複?我應該從 namedtuple 繼承嗎?

<回答>

顯示地將參數複製到屬性中的方式並沒什麼錯誤。如果定義了太多參數要這麼做,有時意味著代碼壞味,你可能需要將這些參數組合成更少的對象。其他時候,這是必要的,而且並無不妥。不管怎麼說,顯示地這麼做是對的。

不過,既然問了如何去避免(而不是它是否必要避免),以下是一些解決方案:

1)針對只有關鍵字參數的情況,可簡單使用setattr

class A:n def __init__(self, **kwargs):n for key in kwargs:n setattr(self, key, kwargs[key])nna = A(l=1, d=2)na.l # will return 1na.d # will return 2n

2)針對同時有位置參數和關鍵字參數,使用裝飾器

import decoratornimport inspectnimport sysnnn@decorator.decoratorndef simple_init(func, self, *args, **kws):n """n @simple_initn def __init__(self,a,b,...,z)n dosomething()nn behaves likenn def __init__(self,a,b,...,z)n self.a = an self.b = bn ...n self.z = zn dosomething()n """nn #init_argumentnames_without_self = [a,b,...,z]n if sys.version_info.major == 2:n init_argumentnames_without_self = inspect.getargspec(func).args[1:]n else:n init_argumentnames_without_self = tuple(inspect.signature(func).parameters.keys())[1:]nn positional_values = argsn keyword_values_in_correct_order = tuple(kws[key] for key in init_argumentnames_without_self if key in kws)n attribute_values = positional_values + keyword_values_in_correct_ordernn for attribute_name,attribute_value in zip(init_argumentnames_without_self,attribute_values):n setattr(self,attribute_name,attribute_value)nn # call the original __init__n func(self, *args, **kws)nnnclass Test():n @simple_initn def __init__(self,a,b,c,d=4):n print(self.a,self.b,self.c,self.d)nn#prints 1 3 2 4nt = Test(1,c=2,b=3)n#keeps signaturen#prints [self, a, b, c, d]nif sys.version_info.major == 2:n print(inspect.getargspec(Test.__init__).args)nelse:n print(inspect.signature(Test.__init__))n

4.為什麼Python3中浮點值4*0.1看起來是對的,但是3*0.1則不然?

我知道絕大部分小數沒有精確的浮點表示(Is floating point math broken?)。

但是我不知道問什麼4*0.1能被很好地列印出0.4,但是3*0.1就不行,這兩個值用decimal表示時也很醜:

>>> 3*0.1n0.30000000000000004n>>> 4*0.1n0.4n>>> from decimal import Decimaln>>> Decimal(3*0.1)nDecimal(0.3000000000000000444089209850062616169452667236328125)n>>> Decimal(4*0.1)nDecimal(0.40000000000000002220446049250313080847263336181640625)n

<回答>

簡單地說,因為由於量化(舍入)誤差的存在,3*0.1 != 0.3(而4*0.1 == 0.4是因為2的冪的乘法通常是一個「精確的」操作)。

你可以在Python中使用 .hex 方法來查看數字的內部表示(基本上,是確切的二進位浮點值,而不是十進位的近似值)。 這可以幫助解釋下面發生了什麼。

>>> (0.1).hex()n0x1.999999999999ap-4n>>> (0.3).hex()n0x1.3333333333333p-2n>>> (0.1*3).hex()n0x1.3333333333334p-2n>>> (0.4).hex()n0x1.999999999999ap-2n>>> (0.1*4).hex()n0x1.999999999999ap-2n

0.1是0x1.999999999999a 乘以 2^-4。結尾處的「a」表示數字10 —— 換句話說,二進位浮點中的0.1比「精確」值0.1稍大(因為最終的0x0.99向上舍入為0x0.a)。 當乘以4,也就是2的冪,指數向上移動(從2^-4到2^-2),但是數字不變,所以4*0.1 == 0.4。

但是,當乘以3時,0x0.99和0x0.a0(0x0.07)之間的微小差異放大為0x0.15的錯誤,在最後一個位置顯示為一位錯誤。 這使得0.1*3大於整值0.3。

Python 3中浮點數的repr設計為可以往返的,也就是說,顯示的值應該可以精確地轉換為原始值。 因此,它不能以完全相同的方式顯示0.3和0.1 * 3,或者兩個不同的數字在往返之後是相同的。 所以,Python 3的repr引擎選擇顯示有輕微的有明顯錯誤的結果。

5.當前行的Python代碼能否知道它的縮進嵌套級別嗎?

比如下面這樣的:

print(get_indentation_level())nn print(get_indentation_level())nn print(get_indentation_level())n

我想獲取到這樣的結果:

1n2n3n

代碼能否通過這種方式讀取自身?

我想要的是更多的嵌套部分的代碼的輸出更多的嵌套。 用同的方式,這使得代碼更容易閱讀,也使輸出更容易閱讀。

當然我可以手動實現,使用例如 .format(),但我想到的是一個自定義 print 函數,它將print(i* + string),其中i是縮進級別。這會是一個在終端中產生可讀輸出的快速方法。

有沒有更好的、能避免辛苦的手動格式化的方法來做到這一點?

<回答>

如果你想要嵌套級別的縮進,而不是空格和製表符,事情變得棘手。 例如,在下面的代碼中:

if True:n print(nget_nesting_level())n

對get_nesting_level的調用實際上是嵌套1級,儘管事實上在get_nesting_level的調用行前沒有空格。同時,在下面的代碼中:

print(1,n 2,n get_nesting_level())n

對get_nesting_level的調用是嵌套0級,儘管在它所在行前存在空格。

在下面的代碼中:

if True:n if True:n print(get_nesting_level())nnif True:n print(get_nesting_level())n

對get_nesting_level的兩次調用處於不同的嵌套級別,儘管空格數是一樣的。

在下面的代碼中:

if True: print(get_nesting_level())n

是嵌套0級,還是1級? 在正式語法中,對於INDENT和DEDENT符號,它是0級,但你可能不會有同樣的感覺。

如果你想這樣做,你將必須符號化整個文件,並為INDENT和DEDENT符號計數。tokenize模塊對於這樣的函數非常有用的:

import inspectnimport tokenizenndef get_nesting_level():n caller_frame = inspect.currentframe().f_backn filename, caller_lineno, _, _, _ = inspect.getframeinfo(caller_frame)n with open(filename) as f:n indentation_level = 0n for token_record in tokenize.generate_tokens(f.readline):n token_type, _, (token_lineno, _), _, _ = token_recordn if token_lineno > caller_lineno:n breakn elif token_type == tokenize.INDENT:n indentation_level += 1n elif token_type == tokenize.DEDENT:n indentation_level -= 1n return indentation_leveln

6.為什麼Python的array很慢?

我以為 array.array 比 list 要快,因為array看起來是未裝箱的(unboxed)。

然後,我得到了下面的結果:

In [1]: import arraynnIn [2]: L = list(range(100000000))nnIn [3]: A = array.array(l, range(100000000))nnIn [4]: %timeit sum(L)n1 loop, best of 3: 667 ms per loopnnIn [5]: %timeit sum(A)n1 loop, best of 3: 1.41 s per loopnnIn [6]: %timeit sum(L)n1 loop, best of 3: 627 ms per loopnnIn [7]: %timeit sum(A)n1 loop, best of 3: 1.39 s per loopn

這種區別的原因是什麼?

<回答>

其存儲是「未裝箱的」,但每當你訪問一個元素的時候,Python必須將它「裝箱」(將之嵌入在一個普通的Python對象中),以便做任何事情。 例如,sum(A)遍歷了array,並且一次一個地把每個證書裝箱到一個普通的Python int對象中。這要花費時間。而在sum(L)中,所有的裝箱都已在創建列表時完成了。

所以最後,數組通常較慢,但是相較需要相當少的內存。

----------------------------------------------------------------------------------------------------------------

這是Python 3最近版本的相關代碼,但是相同的基本思想適用於所有CPython實現。

以下是訪問列表項的代碼:

PyObject *nPyList_GetItem(PyObject *op, Py_ssize_t i)n{n /* error checking omitted */n return ((PyListObject *)op) -> ob_item[i];n}n

它做的事很少:somelist [i] 僅僅返回列表中的第i個對象(CPython中的所有Python對象都是指向一個結構體的指針,其初始段符合一個PyObject結構體的結構)。

下面是具有類型代碼 l 的 array 的__getitem__實現:

static PyObject *nl_getitem(arrayobject *ap, Py_ssize_t i)n{n return PyLong_FromLong(((long *)ap->ob_item)[i]);n}n

原始內存被視為本地平台的元素為C long(長整型)的向量;第 i 個C long 被讀出;然後調用PyLong_FromLong() 將本地的C long 包裝(「裝箱」)成Python long 對象(在Python 3中,它消除了Python 2中 int 和 long 之間的區別,實際上顯示為int)。

這個裝箱必須為Python int對象分配新的內存,並將本地的C long的位寫入其中。在原例的上下文中,這個對象的生命周期非常短暫(只是足夠讓sum()將內容添加到總數中),然後需要更多的時間來釋放新的int對象。

這就是速度差異的來源,總是來自於,而且總將來自於CPython的實現。

7.乘以2比移位快?

我本來在看 sorted_containers 的源碼,然後被 這行 驚到了:

self._load, self._twice, self._half = load, load * 2, load >> 1n

這裡的 load 是一個整數。 為什麼在一個位置使用移位,在另一個位乘? 合理的解釋似乎是,比特移位可能比整數除以2快,但是為什麼不用移位替換乘法呢? 我對以下情況做了基準測試:

#1 (乘法,除法)

#2 (移位,移位)

#3 (乘法,移位)

#4 (移位,除法)

並發現#3 始終比其他方式更快:

# self._load, self._twice, self._half = load, load * 2, load >> 1nnimport randomnimport timeitnimport pandas as pdnnx = random.randint(10 ** 3, 10 ** 6)nndef test_naive():n a, b, c = x, 2 * x, x // 2nnndef test_shift():n a, b, c = x, x << 1, x >> 1nnndef test_mixed():n a, b, c = x, x * 2, x >> 1nnndef test_mixed_swaped():n a, b, c = x, x << 1, x // 2nnndef observe(k):n print(k)n return {n naive: timeit.timeit(test_naive),n shift: timeit.timeit(test_shift),n mixed: timeit.timeit(test_mixed),n mixed_swapped: timeit.timeit(test_mixed_swaped),n }nnndef get_observations():n return pd.DataFrame([observe(k) for k in range(100)]) n

問題:

我的測試有效嗎? 如果是,為什麼(乘法,移位)比(移位,移位)快?我是在Ubuntu 14.04上運行Python 3.5。

以上是問題的原始聲明。 Dan Getz在他的回答中提供了一個很好的解釋。

為了完整性,以下是不應用乘法優化時,用更大x的示例說明。

<回答>

這似乎是因為小數字的乘法在CPython 3.5中得到優化,而小數字的左移則沒有。正左移總是創建一個更大的整數對象來存儲結果,作為計算的一部分,而對於測試中使用的排序的乘法,特殊的優化避免了這一點,並創建了正確大小的整數對象。這可以在Python的整數實現的源代碼中看到。

因為Python中的整數是任意精度的,所以它們被存儲為整數「數字(digits)」的數組,每個整數數字的位數有限制。所以在一般情況下,涉及整數的操作不是單個操作,而是需要處理多個「數字」。在pyport.h中,該位限制在64位平台上定義為30位,其他的為15位。 (這裡我將使用30,以使解釋簡單。但是請注意,如果你使用的Python編譯為32位,你的基準的結果將取決於如果 x 是否小於32,768。

當操作的輸入和輸出保持在該30位限制內時,會以優化的方式而不是通常的方式來處理操作。整數乘法實現的開頭如下:

static PyObject *nlong_mul(PyLongObject *a, PyLongObject *b)n{n PyLongObject *z;nn CHECK_BINOP(a, b);nn / *單位乘法的快速路徑* /n if (Py_ABS(Py_SIZE(a)) <= 1 && Py_ABS(Py_SIZE(b)) <= 1) {n stwodigits v = (stwodigits)(MEDIUM_VALUE(a)) * MEDIUM_VALUE(b);n#ifdef HAVE_LONG_LONGn return PyLong_FromLongLong((PY_LONG_LONG)v);n#elsen / *如果沒有long long,我們幾乎肯定n 使用15位數字,所以 v 將適合 long。在n 不太可能發生的情況中,沒有long longn 我們在平台上使用30位數字,一個大 v n 會導致我們使用下面的一般乘法代碼。 * /n if (v >= LONG_MIN && v <= LONG_MAX)n return PyLong_FromLong((long)v);n#endifn }n

因此,當乘以兩個整數(每個整數適用於30位數字)時,這會由CPython解釋器進行的直接乘法,而不是將整數作為數組。(對一個正整數對象調用的MEDIUM_VALUE()會得到其前30位數字。)如果結果符合一個30位數字,PyLong_FromLongLong() 將在相對較少的操作中注意到這一點,並創建一個單數字整數對象來存儲它。

相反,左移位不是這樣優化的,每次左移位會把整數當做一個數組來處理。特別地,如果你閱讀long_lshift()的源碼,在一個小且正的左移位的情況下,如果只需把它的長度截斷成1,總會創建一個2位數的整數對象:

static PyObject *nlong_lshift(PyObject *v, PyObject *w)n{n /*** ... ***/nn wordshift = shiftby / PyLong_SHIFT; /*** 對於小w,是0 ***/n remshift = shiftby - wordshift * PyLong_SHIFT; /*** 對於小w,是w ***/nn oldsize = Py_ABS(Py_SIZE(a)); /*** 對於小v > 0,是1 ***/n newsize = oldsize + wordshift;n if (remshift)n ++newsize; /*** 對於 w > 0, v > 0,newsize至少會變成2 ***/n z = _PyLong_New(newsize);nn /*** ... ***/n}n

整數除法

你沒有問整數整除相比於右位移哪種性能更差,因為這符合你(和我)的期望。但是將小的正數除以另一個小的正數並不像小乘法那樣優化。每個 // 使用函數long_divrem()計算商和餘數。這個餘數是通過小除數的乘法得到的,並存儲在新分配的整數對象中。在這種情況下,它會立即被丟棄。

8.Python中 "(1,) == 1," 的意思是什麼?

我在測試元組結構,然後發現像下面這樣使用 == 操作符時很奇怪:

>>> (1,) == 1,nOut: (False,)n

當我把這兩個表達式賦值給變數,結果又是真值:

>>> a = (1,)n>>> b = 1,n>>> a==bnOut: Truen

這個問題在我看來不同於Python元組尾逗號的語法規則,我是問 == 操作符之間的表達式組。

<回答>

這是操作符優先順序導致的,可閱讀此文檔。

我來告訴你在下次遇到類似問題的是否怎麼查找答案。你可以使用 ast 模塊解構,來了解表達式怎麼解析的:

>>> import astn>>> source_code = (1,) == 1,n>>> print(ast.dump(ast.parse(source_code), annotate_fields=False))nModule([Expr(Tuple([Compare(Tuple([Num(1)], Load()), [Eq()], [Num(1)])], Load()))])n

從這裡我們看到代碼解析成如Tim Peters解釋的那樣:

Module([Expr(n Tuple([n Compare(n Tuple([Num(1)], Load()), n [Eq()], n [Num(1)]n )n ], Load())n)])n

9.Python中什麼時候 hash(n) == n?

我一直在玩Python的hash函數。對於小整數,hash(n)== n 總是成立的。但是對於大數字則不然:

>>> hash(2**100) == 2**100nFalsen

我不感到驚訝,我理解hash取有限範圍的值。這個範圍是多少呢?

我試圖使用二分查找找出使 hash(n) != n 的最小數字

>>> import codejamhelpers # pip install codejamhelpersn>>> help(codejamhelpers.binary_search)nHelp on function binary_search in module codejamhelpers.binary_search:nnbinary_search(f, t)n Given an increasing function :math:`f`, find the greatest non-negative integer :math:`n` such that :math:`f(n) le t`. If :math:`f(n) > t` for all :math:`n ge 0`, return None.nn>>> f = lambda n: int(hash(n) != n)n>>> n = codejamhelpers.binary_search(f, 0)n>>> hash(n)n2305843009213693950n>>> hash(n+1)n0n

2305843009213693951有什麼特別之處?我注意到它小於sys.maxsize == 9223372036854775807

編輯:我使用的是Python 3。我在Python 2上運行相同的二分查找,得到的是不同的結果2147483648,我注意到這等於 sys.maxint + 1。

我也使用了 [hash(random.random()) for i in range(10 ** 6)] 來估計hash函數的範圍。最大值始終低於上面的n。對比min,Python 3的hash值似乎總是正的,而Python 2的hash值可以是負的。

<回答>

2305843009213693951是2^61-1,它是最大的Mersenne素數,適合64位。

如果你必須通過使用hash產生一個數,併除以某個數來取余,那麼大的Mersenne素數是一個好的選擇 —— 它很容易計算,並確保均勻分布的可能性。 (雖然我個人不會這樣做hash)

用它來計算浮點數的模量特別方便。 它們具有將整數乘以2^x的指數分量。 由於2^61 = 1 mod 2^61-1,你只需要考慮 "(指數) mod 61"。

閱讀: Mersenne prime - Wikipedia

10.Python的字元串中為什麼3個反斜杠和4個反斜杠是相等的?

能告訴我為什麼 ??==?? 的結果是 True 嗎?

>>> list(??)n[?, , , ?]n>>> list(??)n[?, , , ?]n

<回答>

基本上是因為Python在反斜杠處理略微寬鬆。 引自2.4.1 String literals:

與標準C不同,所有無法識別的轉義序列在字元串中保持不變,比如,反斜杠會留在字元串中。

(原文強調)

因此,在python中,不是3個反斜杠等於4個。而是當你在反斜杠後跟類似「?」這樣的字元,這兩個會一起作為兩個字元。因為 「?」 不是可識別的轉義序列。


推薦閱讀:

用Python抓取新版裁判文書網(附代碼,針對初學者修訂)
自學php或python到什麼程度才能找到工作?
個人充電,想學一門替代python的語言?

TAG:Python | StackOverflow |