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

JefHinton 7年前發布 | 16K 次閱讀 Python Python開發

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

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

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

def check_all_conditions():
    x = check_size()
    if x:
        return x

    x = check_color()
    if x:
        return x

    x = check_tone()
    if x:
        return x

    x = check_flavor()
    if x:
        return x
    return None

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

x and return x

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

<回答>

1)使用循環

conditions = (check_size, check_color, check_tone, check_flavor)
for condition in conditions:
    result = condition()
    if result:
        return result

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

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

try:
    # Python 2
    from future_builtins import map, filter
except ImportError:
    # Python 3
    pass

conditions = (check_size, check_color, check_tone, check_flavor)
return next(filter(None, map(lambda f: f(), conditions)), None)

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

3)使用生成器表達式

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

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

def check_all_conditions():
    return check_size() or check_color() or check_tone() or check_flavor() or None

Demo:

>>> x = [] or 0 or {} or -1 or None
>>> x
-1
>>> x = [] or 0 or {} or '' or None
>>> x is None
True

2.如何理解Python循環中的“else”子句?

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

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

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

我怪在關鍵字選擇上持續的不確定性:我發現 else 這個語義簡直難以記憶。我的問題不是“為什么是這個關鍵字用于這個目的”(雖然只閱讀了答案和評論,我可能會投票關閉),而是, 我怎樣思考 else 關鍵字以明白其語義, 那樣我就能記住它了?

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

<回答>

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

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

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

for loops behave the same way. Just consider the condition as true if the iterator has more elements, or false otherwise.

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

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

def __init__(self, x, y, z):
    ...
    self.x = x
    self.y = y
    self.z = z
    ...

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

<回答>

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

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

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

class A:
    def __init__(self, **kwargs):
        for key in kwargs:
          setattr(self, key, kwargs[key])

a = A(l=1, d=2)
a.l # will return 1
a.d # will return 2

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

import decorator
import inspect
import sys


@decorator.decorator
def simple_init(func, self, *args, **kws):
    """
    @simple_init
    def __init__(self,a,b,...,z)
        dosomething()

    behaves like

    def __init__(self,a,b,...,z)
        self.a = a
        self.b = b
        ...
        self.z = z
        dosomething()
    """

    #init_argumentnames_without_self = ['a','b',...,'z']
    if sys.version_info.major == 2:
        init_argumentnames_without_self = inspect.getargspec(func).args[1:]
    else:
        init_argumentnames_without_self = tuple(inspect.signature(func).parameters.keys())[1:]

    positional_values = args
    keyword_values_in_correct_order = tuple(kws[key] for key in init_argumentnames_without_self if key in kws)
    attribute_values = positional_values + keyword_values_in_correct_order

    for attribute_name,attribute_value in zip(init_argumentnames_without_self,attribute_values):
        setattr(self,attribute_name,attribute_value)

    # call the original __init__
    func(self, *args, **kws)


class Test():
    @simple_init
    def __init__(self,a,b,c,d=4):
        print(self.a,self.b,self.c,self.d)

#prints 1 3 2 4
t = Test(1,c=2,b=3)
#keeps signature
#prints ['self', 'a', 'b', 'c', 'd']
if sys.version_info.major == 2:
    print(inspect.getargspec(Test.__init__).args)
else:
    print(inspect.signature(Test.__init__))

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

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

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

>>> 3*0.1
0.30000000000000004
>>> 4*0.1
0.4
>>> from decimal import Decimal
>>> Decimal(3*0.1)
Decimal('0.3000000000000000444089209850062616169452667236328125')
>>> Decimal(4*0.1)
Decimal('0.40000000000000002220446049250313080847263336181640625')

<回答>

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

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

>>> (0.1).hex()
'0x1.999999999999ap-4'
>>> (0.3).hex()
'0x1.3333333333333p-2'
>>> (0.1*3).hex()
'0x1.3333333333334p-2'
>>> (0.4).hex()
'0x1.999999999999ap-2'
>>> (0.1*4).hex()
'0x1.999999999999ap-2'

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())

    print(get_indentation_level())

        print(get_indentation_level())

我想獲取到這樣的結果:

1
2
3

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

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

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

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

<回答>

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

if True:
    print(
get_nesting_level())

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

print(1,
      2,
      get_nesting_level())

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

在下面的代碼中:

if True:
  if True:
    print(get_nesting_level())

if True:
    print(get_nesting_level())

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

在下面的代碼中:

if True: print(get_nesting_level())

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

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

import inspect
import tokenize

def get_nesting_level():
    caller_frame = inspect.currentframe().f_back
    filename, caller_lineno, _, _, _ = inspect.getframeinfo(caller_frame)
    with open(filename) as f:
        indentation_level = 0
        for token_record in tokenize.generate_tokens(f.readline):
            token_type, _, (token_lineno, _), _, _ = token_record
            if token_lineno > caller_lineno:
                break
            elif token_type == tokenize.INDENT:
                indentation_level += 1
            elif token_type == tokenize.DEDENT:
                indentation_level -= 1
        return indentation_level

6.為什么Python的array很慢?

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

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

In [1]: import array

In [2]: L = list(range(100000000))

In [3]: A = array.array('l', range(100000000))

In [4]: %timeit sum(L)
1 loop, best of 3: 667 ms per loop

In [5]: %timeit sum(A)
1 loop, best of 3: 1.41 s per loop

In [6]: %timeit sum(L)
1 loop, best of 3: 627 ms per loop

In [7]: %timeit sum(A)
1 loop, best of 3: 1.39 s per loop

這種區別的原因是什么?

<回答>

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

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

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

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

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

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

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

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

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

原始內存被視為本地平臺的元素為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 >> 1

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

#1 (乘法,除法)

#2 (移位,移位)

#3 (乘法,移位)

#4 (移位,除法)

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

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

import random
import timeit
import pandas as pd

x = random.randint(10 ** 3, 10 ** 6)

def test_naive():
    a, b, c = x, 2 * x, x // 2


def test_shift():
    a, b, c = x, x << 1, x >> 1


def test_mixed():
    a, b, c = x, x * 2, x >> 1


def test_mixed_swaped():
    a, b, c = x, x << 1, x // 2


def observe(k):
    print(k)
    return {
        'naive': timeit.timeit(test_naive),
        'shift': timeit.timeit(test_shift),
        'mixed': timeit.timeit(test_mixed),
        'mixed_swapped': timeit.timeit(test_mixed_swaped),
    }


def get_observations():
    return pd.DataFrame([observe(k) for k in range(100)])

問題:

我的測試有效嗎? 如果是,為什么(乘法,移位)比(移位,移位)快?我是在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 *
long_mul(PyLongObject *a, PyLongObject *b)
{
    PyLongObject *z;

    CHECK_BINOP(a, b);

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

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

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

static PyObject *
long_lshift(PyObject *v, PyObject *w)
{
    /*** ... ***/

    wordshift = shiftby / PyLong_SHIFT;   /*** 對于小w,是0 ***/
    remshift  = shiftby - wordshift * PyLong_SHIFT;   /*** 對于小w,是w ***/

    oldsize = Py_ABS(Py_SIZE(a));   /*** 對于小v > 0,是1 ***/
    newsize = oldsize + wordshift;
    if (remshift)
        ++newsize;   /*** 對于 w > 0, v > 0,newsize至少會變成2 ***/
    z = _PyLong_New(newsize);

    /*** ... ***/
}

整數除法

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

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

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

>>>  (1,) == 1,
Out: (False,)

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

>>> a = (1,)
>>> b = 1,
>>> a==b
Out: True

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

<回答>

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

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

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

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

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

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

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

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

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

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

>>> import codejamhelpers # pip install codejamhelpers
>>> help(codejamhelpers.binary_search)
Help on function binary_search in module codejamhelpers.binary_search:

binary_search(f, t)
    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.

>>> f = lambda n: int(hash(n) != n)
>>> n = codejamhelpers.binary_search(f, 0)
>>> hash(n)
2305843009213693950
>>> hash(n+1)
0

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('?\\\?')
['?', '\\', '\\', '?']
>>> list('?\\\\?')
['?', '\\', '\\', '?']

<回答>

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

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

(原文強調)

因此,在python中,不是3個反斜杠等于4個。而是當你在反斜杠后跟類似“?”這樣的字符,這兩個會一起作為兩個字符。因為 “\?” 不是可識別的轉義序列。

 

來自:https://zhuanlan.zhihu.com/p/25020763

 

 本文由用戶 JefHinton 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!