map()
リスト内包表記またはその逆を使用することを好む理由はありますか? それらのどちらかが一般的に効率的ですか、それとも一般的に他のものよりもpythonicと見なされますか?
13 に答える
map
場合によっては、微視的に高速になる場合があります(目的のためにラムダを作成していないが、マップとリストコンプで同じ関数を使用している場合)。他のケースではリスト内包表記の方が高速である可能性があり、ほとんどの (すべてではない) pythonistas は、リスト内包表記の方がより直接的で明確であると考えています。
まったく同じ関数を使用した場合の map の小さな速度の利点の例:
$ python -m timeit -s'xs=range(10)' 'map(hex, xs)'
100000 loops, best of 3: 4.86 usec per loop
$ python -m timeit -s'xs=range(10)' '[hex(x) for x in xs]'
100000 loops, best of 3: 5.58 usec per loop
マップにラムダが必要な場合にパフォーマンス比較が完全に逆転する例:
$ python -m timeit -s'xs=range(10)' 'map(lambda x: x+2, xs)'
100000 loops, best of 3: 4.24 usec per loop
$ python -m timeit -s'xs=range(10)' '[x+2 for x in xs]'
100000 loops, best of 3: 2.32 usec per loop
ケース
- 一般的なケース: ほとんどの場合、Pythonでリスト内包表記を使用することをお勧めします。これは、初心者のプログラマーがコードを読んで何をしているのかがより明確になるためです。(これは、他のイディオムが適用される可能性がある他の言語には適用されません。) リスト内包表記は反復のための Python の事実上の標準であるため、Python プログラマーに対して何をしているのかがさらに明確になります。彼らは期待されています。
- あまり一般的でないケース: ただし、既に関数が定義されている
map
場合は、「非 Pythonic」と見なされますが、多くの場合、 を使用するのが合理的です。たとえば、map(sum, myLists)
は よりエレガント/簡潔です[sum(x) for x in myLists]
。反復するためだけに 2 回入力しなければならないダミー変数 (たとえばsum(x) for x...
orsum(_) for _...
または) を作成する必要がないという優雅さが得られます。sum(readableName) for readableName...
同じ議論がfilter
andreduce
およびitertools
モジュールのすべてに当てはまります。すでに便利な関数がある場合は、先に進んで関数型プログラミングを行うことができます。これにより、状況によっては可読性が向上し、他の状況 (初心者プログラマー、複数の引数など) では可読性が失われます... しかし、コードの可読性は、とにかくコメントに大きく依存します。 - ほとんどありません: マッピングやカリー
map
化を行っている関数型プログラミングを行っているときに、関数を純粋な抽象関数として使用したい場合や、関数として話すことで利益を得たい場合があります。たとえば、Haskell では、 と呼ばれるファンクター インターフェイスが、任意のデータ構造に対するマッピングを一般化します。これは Python では非常に珍しいことです。なぜなら、Python の文法では反復について話すためにジェネレーター スタイルを使用する必要があるからです。簡単に一般化することはできません。(これは良いこともあれば悪いこともあります。)合理的なことを行うまれな python の例を思い付くことができるでしょう。私が思いつく最も近い例は、次のものとほぼ同等のワンライナーです。map
map
map
fmap
map(f, *lists)
sumEach = partial(map,sum)
def sumEach(myLists):
return [sum(_) for _ in myLists]
for
-loopを使用するだけ: もちろん、for ループを使用することもできます。関数型プログラミングの観点からはそれほど洗練されていませんが、非ローカル変数を使用すると、python などの命令型プログラミング言語でコードがより明確になることがあります。一般に、for ループは、リスト内包表記のようなリストを作成しない複雑な操作を行っているだけの場合に最も効率的であり、マップは最適化されています (たとえば、合計、ツリーの作成など)。少なくともメモリの観点から効率的です(必ずしも時間の観点からではありません。最悪の場合、まれな病理学的ガベージコレクションの問題を除いて、一定の要因が予想されます)。
「パイソン主義」
私は「pythonic」という言葉が好きではありません. それにもかかわらず、map
およびfilter
類似の関数 (非常に便利なitertools
モジュールなど) は、おそらくスタイルの点で非 Pythonic と見なされます。
怠惰
効率という点では、ほとんどの関数型プログラミング構造と同様に、MAP CAN BE LAZYであり、実際に Python では遅延です。つまり、これを ( python3で) 行うことができ、コンピューターのメモリが不足したり、保存されていないデータがすべて失われたりすることはありません。
>>> map(str, range(10**100))
<map object at 0x2201d50>
リスト内包表記でそれを試してください:
>>> [str(n) for n in range(10**100)]
# DO NOT TRY THIS AT HOME OR YOU WILL BE SAD #
リスト内包表記も本質的に遅延ですが、python はそれらを非遅延として実装することを選択したことに注意してください。それにもかかわらず、Python は、次のように、ジェネレーター式の形式で遅延リスト内包表記をサポートしています。
>>> (str(n) for n in range(10**100))
<generator object <genexpr> at 0xacbdef>
[...]
基本的に、この構文は、ジェネレーター式をリスト コンストラクターに渡すものと考えることができますlist(x for x in range(5))
。
簡単な考案例
from operator import neg
print({x:x**2 for x in map(neg,range(5))})
print({x:x**2 for x in [-y for y in range(5)]})
print({x:x**2 for x in (-y for y in range(5))})
リスト内包表記は遅延がないため、より多くのメモリが必要になる場合があります (ジェネレーター内包表記を使用しない場合)。角括弧[...]
は、特に括弧がごちゃごちゃしている場合に、物事を明確にすることがよくあります。一方、入力のように冗長になってしまうこともあります[x for x in...
。イテレータ変数を短くしておく限り、コードをインデントしないと、リスト内包表記は通常より明確になります。ただし、いつでもコードをインデントできます。
print(
{x:x**2 for x in (-y for y in range(5))}
)
または物事を分割します:
rangeNeg5 = (-y for y in range(5))
print(
{x:x**2 for x in rangeNeg5}
)
python3 の効率比較
map
は今怠惰です:
% python3 -mtimeit -s 'xs=range(1000)' 'f=lambda x:x' 'z=map(f,xs)'
1000000 loops, best of 3: 0.336 usec per loop ^^^^^^^^^
したがって、すべてのデータを使用しない場合、または事前に必要なデータ量がわからない場合、map
python3 (および python2 または python3 のジェネレーター式) では、必要な最後の瞬間まで値の計算を回避します。通常、これは を使用することによるオーバーヘッドを上回りますmap
。欠点は、ほとんどの関数型言語とは対照的に、Python ではこれが非常に制限されていることです。Python ジェネレーターの式は order でのみ評価できるため、データを左から右に「順番に」アクセスする場合にのみ、この利点が得られますx[0], x[1], x[2], ...
。
f
しかし、にしたい事前に作成された関数があり、 ですぐに評価を強制することmap
で の怠惰を無視するとしましょう。非常に興味深い結果が得られます。map
list(...)
% python3 -mtimeit -s 'xs=range(1000)' 'f=lambda x:x' 'z=list(map(f,xs))'
10000 loops, best of 3: 165/124/135 usec per loop ^^^^^^^^^^^^^^^
for list(<map object>)
% python3 -mtimeit -s 'xs=range(1000)' 'f=lambda x:x' 'z=[f(x) for x in xs]'
10000 loops, best of 3: 181/118/123 usec per loop ^^^^^^^^^^^^^^^^^^
for list(<generator>), probably optimized
% python3 -mtimeit -s 'xs=range(1000)' 'f=lambda x:x' 'z=list(f(x) for x in xs)'
1000 loops, best of 3: 215/150/150 usec per loop ^^^^^^^^^^^^^^^^^^^^^^
for list(<generator>)
結果は AAA/BBB/CCC の形式で、A は 2010 年頃の Intel ワークステーションで Python 3.?.? で実行され、B と C は 2013 年頃の AMD ワークステーションで python 3.2.1 で実行されました。非常に異なるハードウェアで。その結果、マップ内包表記とリスト内包表記のパフォーマンスは同等であると思われますが、これは他のランダムな要因の影響を最も強く受けます。私たちが言える唯一のことは、奇妙なことに、リスト内包表記は[...]
ジェネレーター式よりも優れたパフォーマンスを期待している一方で(...)
、ジェネレーター式よりもmap
効率的であるということです (すべての値が評価/使用されると仮定します)。
これらのテストは非常に単純な関数 (恒等関数) を想定していることを理解することが重要です。ただし、関数が複雑な場合、パフォーマンスのオーバーヘッドはプログラム内の他の要因と比較して無視できるため、これは問題ありません。( のような他の単純なものでテストすることはまだ興味深いかもしれませんf=lambda x:x+x
)
Python アセンブリを読むのに熟練している場合は、dis
モジュールを使用して、実際に舞台裏で何が起こっているかを確認できます。
>>> listComp = compile('[f(x) for x in xs]', 'listComp', 'eval')
>>> dis.dis(listComp)
1 0 LOAD_CONST 0 (<code object <listcomp> at 0x2511a48, file "listComp", line 1>)
3 MAKE_FUNCTION 0
6 LOAD_NAME 0 (xs)
9 GET_ITER
10 CALL_FUNCTION 1
13 RETURN_VALUE
>>> listComp.co_consts
(<code object <listcomp> at 0x2511a48, file "listComp", line 1>,)
>>> dis.dis(listComp.co_consts[0])
1 0 BUILD_LIST 0
3 LOAD_FAST 0 (.0)
>> 6 FOR_ITER 18 (to 27)
9 STORE_FAST 1 (x)
12 LOAD_GLOBAL 0 (f)
15 LOAD_FAST 1 (x)
18 CALL_FUNCTION 1
21 LIST_APPEND 2
24 JUMP_ABSOLUTE 6
>> 27 RETURN_VALUE
>>> listComp2 = compile('list(f(x) for x in xs)', 'listComp2', 'eval')
>>> dis.dis(listComp2)
1 0 LOAD_NAME 0 (list)
3 LOAD_CONST 0 (<code object <genexpr> at 0x255bc68, file "listComp2", line 1>)
6 MAKE_FUNCTION 0
9 LOAD_NAME 1 (xs)
12 GET_ITER
13 CALL_FUNCTION 1
16 CALL_FUNCTION 1
19 RETURN_VALUE
>>> listComp2.co_consts
(<code object <genexpr> at 0x255bc68, file "listComp2", line 1>,)
>>> dis.dis(listComp2.co_consts[0])
1 0 LOAD_FAST 0 (.0)
>> 3 FOR_ITER 17 (to 23)
6 STORE_FAST 1 (x)
9 LOAD_GLOBAL 0 (f)
12 LOAD_FAST 1 (x)
15 CALL_FUNCTION 1
18 YIELD_VALUE
19 POP_TOP
20 JUMP_ABSOLUTE 3
>> 23 LOAD_CONST 0 (None)
26 RETURN_VALUE
>>> evalledMap = compile('list(map(f,xs))', 'evalledMap', 'eval')
>>> dis.dis(evalledMap)
1 0 LOAD_NAME 0 (list)
3 LOAD_NAME 1 (map)
6 LOAD_NAME 2 (f)
9 LOAD_NAME 3 (xs)
12 CALL_FUNCTION 2
15 CALL_FUNCTION 1
18 RETURN_VALUE
[...]
よりも構文を使用する方が良いようですlist(...)
。悲しいことに、このmap
クラスは逆アセンブルに対して少し不透明ですが、速度テストで対応できます。
Python 2:リスト内包表記の代わりにmap
andを使用する必要があります。filter
「Pythonic」ではないにもかかわらず、それらを優先する客観的な理由は次のとおりです。新しいスコープ
を導入する引数として関数/ラムダが必要です。
私はこれに何度も噛まれました:
for x, y in somePoints:
# (several lines of code here)
squared = [x ** 2 for x in numbers]
# Oops, x was silently overwritten!
しかし、代わりに私が言った場合:
for x, y in somePoints:
# (several lines of code here)
squared = map(lambda x: x ** 2, numbers)
その後、すべてがうまくいったでしょう。
同じスコープで同じ変数名を使用するのはばかげていると言えます。
私はそうではありませんでした。コードは元々問題x
ありませんでした。2 つの は同じスコープにありませんでした。問題が発生したのは、内側のブロックをコードの別のセクションに移動
した
後でした (読み取り: 開発中の問題ではなく、メンテナンス中の問題)。
はい、この間違いを犯さなければ、リスト内包表記はより洗練されたものになります。
しかし、個人的な経験から (そして他の人が同じ間違いを犯しているのを見て)、これらのバグがコードに忍び寄ったときに経験しなければならない苦痛に値しないと思うほど、何度もそれが起こるのを見てきました。
結論:
とを使用map
しfilter
ます。それらは、スコープ関連の微妙な診断困難なバグを防ぎます。
サイドノート:
状況に適している場合は、imap
and ifilter
(in ) の使用を検討することを忘れないでください!itertools
実際、map
Python 3 言語では、リスト内包表記の動作がまったく異なります。次の Python 3 プログラムを見てください。
def square(x):
return x*x
squares = map(square, [1, 2, 3])
print(list(squares))
print(list(squares))
"[1, 4, 9]" という行が 2 回出力されることを期待するかもしれませんが、代わりに "[1, 4, 9]" の後に "[]" が出力されます。最初squares
は 3 つの要素のシーケンスとして動作するように見えますが、2 回目は空の要素として動作します。
Python 2 言語でmap
は、リスト内包表記が両方の言語で行うように、単純な古いリストを返します。map
重要なのは、Python 3 (およびPython 2)の戻り値がimap
リストではなく、イテレータであることです!
リストを反復する場合とは異なり、反復子を反復する場合、要素は消費されます。これが、最後の行squares
で空に見える理由です。print(list(squares))
要約する:
- イテレータを扱うときは、イテレータがステートフルであり、トラバースすると変化することを覚えておく必要があります。
- リストは、明示的に変更した場合にのみ変更されるため、より予測可能です。それらはコンテナです。
- おまけに、数値、文字列、およびタプルは、まったく変更できないため、さらに予測可能です。それらは値です。
非同期、並列、または分散コードを作成する予定がある場合は、おそらくmap
リスト内包表記よりも優先されるでしょう。ほとんどの非同期、並列、または分散パッケージは、map
python のmap
. 次に、適切な関数をコードの残りの部分に渡すことmap
で、元のシリアル コードを変更して並行して実行する必要がなくなる場合があります (など)。
考えられるケースの 1 つを次に示します。
map(lambda op1,op2: op1*op2, list1, list2)
対:
[op1*op2 for op1,op2 in zip(list1,list2)]
zip() は、マップの代わりにリスト内包表記を使用することを主張する場合に甘やかす必要がある不幸で不必要なオーバーヘッドであると推測しています。肯定的であろうと否定的であろうと、誰かがこれを明確にすると素晴らしいでしょう。
リスト内包表記は一般的に、私がやろうとしていることよりも表現力が高いことがわかりましたmap
-どちらもそれを成し遂げますが、前者は複雑なlambda
表現である可能性があるものを理解しようとする精神的負担を軽減します。
Guido がlambda
s と関数型関数を Python に受け入れたことを最も後悔しているものとしてリストしているどこかのインタビューもあります (私はそれを直接見つけることはできません)。その。
したがって、Python 3はイテレーターであるため、イテレーターまたはオブジェクトmap()
のどちらが必要かを覚えておく必要があります。list
@AlexMartelli が既に述べたように、関数map()
を使用しない場合にのみ、リスト内包表記よりも高速ですlambda
。
時間の比較をいくつか紹介します。
Python 3.5.2
と CPython Jupiter ノートブックと特に組み込みのマジック コマンド
を使用しました%timeit
設定:
x_list = [(i, i+1, i+2, i*2, i-9) for i in range(1000)]
i_list = list(range(1000))
組み込み関数:
%timeit map(sum, x_list) # creating iterator object
# Output: The slowest run took 9.91 times longer than the fastest.
# This could mean that an intermediate result is being cached.
# 1000000 loops, best of 3: 277 ns per loop
%timeit list(map(sum, x_list)) # creating list with map
# Output: 1000 loops, best of 3: 214 µs per loop
%timeit [sum(x) for x in x_list] # creating list with list comprehension
# Output: 1000 loops, best of 3: 290 µs per loop
lambda
関数:
%timeit map(lambda i: i+1, i_list)
# Output: The slowest run took 8.64 times longer than the fastest.
# This could mean that an intermediate result is being cached.
# 1000000 loops, best of 3: 325 ns per loop
%timeit list(map(lambda i: i+1, i_list))
# Output: 1000 loops, best of 3: 183 µs per loop
%timeit [i+1 for i in i_list]
# Output: 10000 loops, best of 3: 84.2 µs per loop
ジェネレーター式などもあります。 PEP-0289を参照してください。だから私はそれを比較に追加すると便利だと思いました
%timeit (sum(i) for i in x_list)
# Output: The slowest run took 6.66 times longer than the fastest.
# This could mean that an intermediate result is being cached.
# 1000000 loops, best of 3: 495 ns per loop
%timeit list((sum(x) for x in x_list))
# Output: 1000 loops, best of 3: 319 µs per loop
%timeit (i+1 for i in i_list)
# Output: The slowest run took 6.83 times longer than the fastest.
# This could mean that an intermediate result is being cached.
# 1000000 loops, best of 3: 506 ns per loop
%timeit list((i+1 for i in i_list))
# Output: 10000 loops, best of 3: 125 µs per loop
list
オブジェクトが必要です:
list(map())
カスタム関数ならリスト内包表記、組み込み関数ならリスト内包表記を使う
オブジェクトは必要ありません。反復可能なlist
オブジェクトが必要なだけです。
常に使用してmap()
ください!