4

Pythonでのさまざまな同等の形式を比較しfilter(xs, lambda x: x != el)ているときに、私は驚いたことに気づきました。次のフォームを検討してください。

def method1(xs, el):
    p = lambda x: x != el
    return [x for x in xs if p(x)]

def method2(xs, el):
    return [x for x in xs if (lambda y: y != el)(x)]

Pythonがラムダを1回だけビルドし、それを一時変数に格納して、両方のフォームがほぼ同様に機能するようにすることを期待します。名前の検索が原因で、それでもmethod1パフォーマンスが低下する可能性があります。

しかし、私がそれらをベンチマークしたとき、それはmethod2一貫してより悪いパフォーマンスであることがわかりましたmethod1。どうしてこれなの?反復ごとにラムダを再構築していますか?


私のベンチマークスクリプト(別のモジュールにあり、とを含むことを期待しmethodsています)は次のとおりです。method1method2

import math, timeit

def bench(n,rho,z):
    pre = """\
import random
from methods import %(method)s

x = [(random.randint(0,%(domain)i)) for r in xrange(%(size)i)]
el = x[0]\
"""

    def testMethod(m):
        mod = pre % { 'method': m, 'domain': int(math.ceil(n / rho)), 'size': n }
        return timeit.timeit("%s(x, el)" % m, mod, number = z)/(z * n)

    print "Testing", n, rho, z
    return tuple(testMethod(m) for m in ("method1", "method2"))

n = 31

min_size, max_size = 10.0**1, 10.0**4
size_base = math.pow(max_size / min_size, 1.0/(n-1))
# size_default = 10**3

#min_sel, max_sel = 0.001, 1.0
#sel_base = math.pow(max_sel / min_sel, 1.0/(n-1))
sel_default = 0.001

tests = [bench(int(min_size*size_base**x), sel_default, 100) for x in xrange(n)]
#tests = [bench(size_default, min_sel*sel_base**x, 100) for x in xrange(n)]

def median(x):
    x = list(sorted(x))
    mi = int(len(x)/2)
    if n % 2 == 0:
        return x[mi]
    else:
        return (x[mi] + x[mi+1])/2

def madAndMedian(x):
    meh = median(x)
    return meh, median([abs(xx - meh) for xx in x])

for z in zip(*tests):
    print madAndMedian(z)
4

2 に答える 2

4

はい、ループごとにラムダを再構築しています。その式全体を再評価する必要があります。

これを確認するには、disモジュールを使用します:

>>> dis.dis(method1)
  2           0 LOAD_CLOSURE             0 (el)
              3 BUILD_TUPLE              1
              6 LOAD_CONST               1 (<code object <lambda> at 0x102000230, file "<stdin>", line 2>)
              9 MAKE_CLOSURE             0
             12 STORE_FAST               2 (p)

  3          15 BUILD_LIST               0
             18 LOAD_FAST                0 (xs)
             21 GET_ITER            
        >>   22 FOR_ITER                24 (to 49)
             25 STORE_FAST               3 (x)
             28 LOAD_FAST                2 (p)
             31 LOAD_FAST                3 (x)
             34 CALL_FUNCTION            1
             37 POP_JUMP_IF_FALSE       22
             40 LOAD_FAST                3 (x)
             43 LIST_APPEND              2
             46 JUMP_ABSOLUTE           22
        >>   49 RETURN_VALUE        
>>> dis.dis(method2)
  2           0 BUILD_LIST               0
              3 LOAD_FAST                0 (xs)
              6 GET_ITER            
        >>    7 FOR_ITER                33 (to 43)
             10 STORE_FAST               2 (x)
             13 LOAD_CLOSURE             0 (el)
             16 BUILD_TUPLE              1
             19 LOAD_CONST               1 (<code object <lambda> at 0x101fd37b0, file "<stdin>", line 2>)
             22 MAKE_CLOSURE             0
             25 LOAD_FAST                2 (x)
             28 CALL_FUNCTION            1
             31 POP_JUMP_IF_FALSE        7
             34 LOAD_FAST                2 (x)
             37 LIST_APPEND              2
             40 JUMP_ABSOLUTE            7
        >>   43 RETURN_VALUE        

LOAD_CONSTオペコードは、ラムダ本体のコンパイル済みコードをロードします。MAKE_CLOSUREそこからラムダを作成します。method1これは 1 回発生しますが、これmethod2はループの各反復 (FOR_ITERオペコードからオペコードへJUMP_ABSOLUTE) で繰り返されます。代わりにローカル変数を参照する変数のLOAD_FASTオペコードに注意してください。pmethod1

于 2013-01-09T12:52:35.737 に答える
2

Python は、ラムダのコードを 1 回だけビルドします。ただし、ループの各パスで新しい関数オブジェクト (変数を含むコードとそれを囲む環境を参照する) を構築します。el各関数オブジェクトには、 を介して異なるプロパティが割り当てられる可能性があるため、これは通常は機能__setitem__です。

この場合、関数オブジェクトはどこにも格納されておらず、スコープ外にも漏れていないため、その作成をループの外に移動しても安全ですが、Python のコンパイラはまだそのような最適化を実行できるほどスマートではありません。

于 2013-01-09T12:57:05.103 に答える