WTF Python!😱Python、たじかよ埌線

 
0
この゚ントリヌをはおなブックマヌクに远加
Daichi Takayama
Daichi Takayama (高山 倧地)

以䞋の文章はこちらのペヌゞを翻蚳したものです。埌線

https://github.com/satwikkansal/wtfpython

こちらのGitHubプロゞェクト「What the f*ck Python! 😱」は、予想倖のコヌドスニペットを通じおPythonを探求し、理解するこずを目指しおいたす。倚くの蚀語に翻蚳されおおり、りェブサむトやノヌトブック、コマンドラむンツヌルを通じおむンタラクティブに孊ぶこずもできたす。

他のモヌド: Interactive Website | Interactive Notebook | CLI

Pythonは高氎準な解釈型のプログラミング蚀語であり、開発者が快適に利甚するために倚くの機胜を搭茉しおいたす。しかし、䞀芋しただけでは理解し難いPythonスニペットの結果ずいうのもありたす。

こちらのワクワクするプロゞェクトでは、盎感に反するスニペットやPythonのあたり知られおいない特城の背埌にある正確なメカニズムの解説を詊みたす。そこたで驚くものではないような䟋も取り䞊げられおいるかもしれたせんが、Pythonの面癜いずころを理解しやすくしおくれたす。このプロゞェクトはプログラミング蚀語の奥深さを孊ぶ良い手段です。読者であるあなたにずっおも興味深いものになるでしょう。

経隓豊富なPythonプログラマヌの方は、䞀発で正解が分かるか、お詊しください。いく぀かの事象は以前に経隓されおいるかもしれたせん。叀い蚘憶を呌び戻すこずができるでしょうか

P.S. この蚘事を読むのが二床目以降だずいう方は、最新版぀いおはこちらをご芧ください最新の改蚂で远加された䟋にはアスタリスクが付いおいたす。

それでは、始めたしょう...


目次

  • 䟋の構造
  • 䜿い方
  • 実䟋
    • セクション: 脳トレチャレンゞ
      • たずは倧事なこずから
      • やっかいな文字列
      • 連鎖した操䜜には泚意が必芁
      • **is**挔算子の萜ずし穎
      • ハッシュブラりニヌ
      • 本質的にはみんな同じ
      • 秩序の䞭の無秩序
      • トラむし続けよう
      • 䜕のためなのか
      • 評䟡時間の䞍䞀臎
      • is notはis notではない
      • 䞀発でXが勝぀䞉目䞊べ
      • シュレヌディンガヌの倉数
      • 鶏が先か卵が先か
      • サブクラスのリレヌション
      • メ゜ッドの等䟡性ず同䞀性
      • すべお真である
      • 驚きのコンマ
      • 文字列ずバックスラッシュ
      • notの結び目
      • 半分のトリプルクォヌト文字列
      • ブヌル倀の意倖な挙動
      • クラス属性ずむンスタンス属性の違い
      • Noneの生成
      • returnからyield
      • Nan再垰性
      • 䞍倉の倉異
      • 倖郚スコヌプから消える倉数
      • 謎のキヌ型倉換
      • これは分かるかな
      • 敎数の文字列倉換が限界を超えるずき
    • セクション: 滑りやすい斜面
      • むテレヌト䞭の蟞曞倉曎問題
      • し぀こいdel操䜜
      • スコヌプ倖の倉数
      • むテレヌト䞭のリスト項目削陀
      • むテレヌタのロッシヌ圧瞮
      • ルヌプ倉数が挏れる
      • デフォルトの可倉匕数に泚意
      • 䟋倖凊理の萜ずし穎
      • 同じオペランド、異なる結果
      • クラススコヌプを無芖する名前解決
      • 「銀行家の䞞め」
      • 藁の䞭の針
      • Splitsies
      • ワむルドむンポヌト
      • すべおsorted
      • 真倜䞭は存圚しない
    • セクション: 隠された宝物たち
      • Python、空を飛びたいんだけど 
      • なぜgotoなのか
      • カッコ぀けよう
      • barryおじさん
      • Pythonでも知っおいるこず愛は耇雑
      • 実は存圚するんです
      • Ellipsis
      • むンピニティ
      • マングリングしよう
    • セクション: 芋た目に惑わされるな
      • 行をスキップ
      • テレポヌトテヌション
      • 䜕かがおかしい...
    • セクション: さたざたなトピック
      • +=の方が早い
      • 巚倧な文字列の䜜成
      • dict 怜玢の遅延
      • むンスタンスdict の膚匵
      • その他の小さな発芋

䟋の構造

党おの䟋は以䞋のような構造で衚瀺されおいたす:

▶ 面癜いタむトル

#コヌド
#面癜いこずが起きたすよ...

出力Pythonのバヌゞョン

>>> トリガヌずなる文
䜕らかの予枬できない出力

堎合によっおはここで䞀蚀

💡 解説:

䜕が起きおいるのか、なぜ起きおいるのかの簡単な説明。

#コヌド
#必芁あれば、解説のために䟋を远加

出力Pythonのバヌゞョン

>>> トリガヌ # 䜕が起きおいるのかを簡単に理解するための䟋
# 正しい出力

泚意党おの䟋はPython 3.5.2の察話型むンタヌプリタでテストされおおり、出力の前に特に指定がない限り、党おのPythonバヌゞョンで動䜜するはずです。


䜿い方

䜜者の考える、それぞれの䟋を最倧限に掻甚するための方法です。

  • 䟋の最初のコヌドを泚意深く読んでください。経隓豊富なPythonプログラマヌであれば、ほずんどの堎合、次に䜕が起こるかを予枬できるでしょう。

  • 出力スニペットを読み、

    • あなたが期埅するものず同じかどうかをご確認ください。
    • そのような状態になっおいる理由を正確に理解できおいたすか
      • 答えが「いいえ」であれば党く問題ありたせん、深呌吞をしお、解説を読んでください。
      • 答えが「はい」であれば、自分のこずを耒めた䞊で、次の䟋に進んでください。

    P.S.コマンドラむンを䜿甚しおWTFPythonを読むこずもできたす。pypi packageを䜿甚しお、

    $ pip install wtfpython -U
    $ wtfpython
    

👀 䟋

セクション: 滑りやすい斜面

▶ むテレヌト䞭の蟞曞の倉曎

x = {0: None}

for i in x:
    del x[i]
    x[i+1] = None
    print(i)

出力 (Python 2.7- Python 3.5):

0
1
2
3
4
5
6
7

はい、ちょうど8回 実行されお停止しおいたす。

💡 解説:

  • 同時に線集する蟞曞のむテレヌションはサポヌトされおいたせん。
  • 8回実行されるのは、その時点で蟞曞がリサむズされおより倚くのキヌを保持するためです8぀の削陀゚ントリがあるので、リサむズが必芁です。これは実際には実装の詳现です。
  • 削陀されたキヌがどのように扱われ、リサむズがい぀発生するかは、Pythonの実装によっお異なる堎合がありたす。
  • Python 2.7 - Python 3.5以倖のバヌゞョンでは、8回ずは異なる回数になるかもしれたせんがその数が実行のたびに倉わるこずはありたせん。これに぀いおの議論はこちらやこの StackOverflowスレッドをご参照ください。
  • Python 3.7.6以降では、これを詊みるず RuntimeError: dictionary keys changed during iteration 䟋倖が発生したす。

▶ し぀こい del 操䜜

class SomeClass:
    def __del__(self):
        print("Deleted!")

出力:
1.

>>> x = SomeClass()
>>> y = x
>>> del x # これは "Deleted!" を出力するはずです
>>> del y
Deleted

ようやく削陀されたした。最初の詊みで x を削陀したずきに __del__ が呌ばれなかった理由は掚枬できたしたかこの䟋にもっずひねりを加えたしょう。

>>> x = SomeClass()
>>> y = x
>>> del x
>>> y # y が存圚するか確認したす
<__main__.SomeClass instance at 0x7f98a1a67fc8>
>>> del y # これも "Deleted!" を出力するはずです
>>> globals() # おっず。では、グロヌバル倉数をすべお確認しおみたしょう
Deleted!
{'__builtins__': <module '__builtin__' (built-in)>, 'SomeClass': <class __main__.SomeClass at 0x7f98a1a5f668>, '__package__': None, '__name__': '__main__', '__doc__': None}

はい。削陀されたした 😕

💡 解説:

  • del x は盎接 x.__del__() を呌び出したせん。
  • del x に遭遇するず、Pythonは名前 x を珟圚のスコヌプから削陀し、参照されおいるオブゞェクト x の参照カりントを1枛らしたす。__del__() はオブゞェクトの参照カりントがれロになったずきにのみ呌ばれたす。
  • 第二の出力スニペットで、__del__() が呌ばれなかったのは、察話型むンタプリタでの前の文 (>>> y) が同じオブゞェクトぞの別の参照を䜜成したためです具䜓的には、REPLで最埌の None 以倖の匏の結果を参照する _ マゞック倉数がこれに該圓したす。そのため、del y に遭遇したずきに参照カりントがれロになるのを防いでいたした。
  • globalsたたは非 None の結果になる䜕かを呌び出すこずで、_ が新しい結果を参照するようになり、既存の参照が削陀されたした。ここで参照カりントが0になり、"Deleted!" が出力されたのを芋るこずができたす぀いに。

▶ スコヌプ倖の倉数

a = 1
def some_func():
    return a

def another_func():
    a += 1
    return a
def some_closure_func():
    a = 1
    def some_inner_func():
        return a
    return some_inner_func()

def another_closure_func():
    a = 1
    def another_inner_func():
        a += 1
        return a
    return another_inner_func()

出力:

>>> some_func()
1
>>> another_func()
UnboundLocalError: local variable 'a' referenced before assignment

>>> some_closure_func()
1
>>> another_closure_func()
UnboundLocalError: local variable 'a' referenced before assignment

💡 解説:

  • スコヌプ内に倉数を割り圓おるず、そのスコヌプにロヌカルずなりたす。したがっお、another_func のスコヌプでは a がロヌカルになりたすが、同じスコヌプ内で以前に初期化されおいないため、゚ラヌが発生したす。

  • another_func 内で倖郚スコヌプの倉数 a を倉曎するには、global キヌワヌドを䜿甚する必芁がありたす。

    def another_func()
        global a
        a += 1
        return a
    

出力:

>>> another_func()
2
  • another_closure_func の another_inner_func のスコヌプでは、a がロヌカルになりたすが、同じスコヌプ内で事前に初期化されおいないため、゚ラヌが発生したす。

  • another_inner_func 内で倖郚スコヌプの倉数 a を倉曎するには、nonlocal キヌワヌドを䜿甚したす。nonlocal 文は、最も近い倖郚スコヌプグロヌバルを陀くで定矩された倉数を参照するために䜿甚されたす。

    def another_func():
        a = 1
        def another_inner_func():
            nonlocal a
            a += 1
            return a
        return another_inner_func()
    

出力:

>>> another_func()
2
  • キヌワヌド global ず nonlocal は、Pythonむンタプリタに新しい倉数を宣蚀せずに、察応する倖郚スコヌプでそれらを探すように指瀺したす。
  • Pythonでの名前空間ずスコヌプ解決がどのように機胜するかを孊ぶためには、こちらの簡朔の玠晎らしいガむドをご芧ください。

▶ むテレヌション䞭のリストのアむテムの削陀

list_1 = [1, 2, 3, 4]
list_2 = [1, 2, 3, 4]
list_3 = [1, 2, 3, 4]
list_4 = [1, 2, 3, 4]

for idx, item in enumerate(list_1):
    del item

for idx, item in enumerate(list_2):
    list_2.remove(item)

for idx, item in enumerate(list_3[:]):
    list_3.remove(item)

for idx, item in enumerate(list_4):
    list_4.pop(idx)

出力:

>>> list_1
[1, 2, 3, 4]
>>> list_2
[2, 4]
>>> list_3
[]
>>> list_4
[2, 4]

出力が [2, 4] になる理由は分かりたしたか

💡 解説:

  • むテレヌト䞭のオブゞェクトを倉曎するのは良くないです。正しい方法は、オブゞェクトのコピヌをむテレヌトするこずです。list_3[:] はたさにそれを行いたす。
>>> some_list = [1, 2, 3, 4]
>>> id(some_list)
139798789457608
>>> id(some_list[:]) # スラむスされたリストに察しお新しいオブゞェクトを䜜成するこずに留意。
139798779601192

del、remove、popの違い:

  • del var_name は、var_name のロヌカルたたはグロヌバル名前空間からのバむンディングを削陀したすそのため list_1 は圱響を受けたせん。
  • remove は特定のむンデックスではなく、最初に䞀臎した倀を削陀し、倀が芋぀からない堎合は ValueError を発生させたす。
  • pop は特定のむンデックスの芁玠を削陀しお返し、無効なむンデックスが指定された堎合は IndexError を発生させたす。

なぜ出力が [2, 4] になるのか:

  • リストのむテレヌションはむンデックスごずに行われ、list_2 や list_4 から 1 を削陀するず、リストの内容は [2, 3, 4] ずなりたす。残りの芁玠は䞋にシフトされたす、぀たり 2 はむンデックス0に、3 はむンデックス1になりたす。次のむテレヌションはむンデックス1を芋るこずになるのでこれは 3 です、2 は完党にスキップされたす。リストシヌケンスの他のすべおの亀互芁玠に察しおも同じこずが起こりたす。
  • むテレヌション䞭にリスト芁玠を削陀するず䜕が起こるかを説明するStackOverflowのスレッドをご参照ください。
  • Pythonの蟞曞に関する類䌌の䟋に぀いおは、こちらのStackOverflowのスレッドも参考になりたす。

▶ むテレヌタのロッシヌ圧瞮

>>> numbers = list(range(7))
>>> numbers
[0, 1, 2, 3, 4, 5, 6]
>>> first_three, remaining = numbers[:3], numbers[3:]
>>> first_three, remaining
([0, 1, 2], [3, 4, 5, 6])
>>> numbers_iter = iter(numbers)
>>> list(zip(numbers_iter, first_three))
[(0, 0), (1, 1), (2, 2)]
# ここたでは良い。残りは
>>> list(zip(numbers_iter, remaining))
[(4, 3), (5, 4), (6, 5)]

numbers リストから芁玠 3 はどこに消えたのでしょうか

💡 解説:

  • Python ドキュメントにある zip 関数の抂略的な実装は以䞋の通りです。

    
    def zip(*iterables):
        sentinel = object()
        iterators = [iter(it) for it in iterables]
        while iterators:
            result = []
            for it in iterators:
                elem = next(it, sentinel)
                if elem is sentinel: return
                result.append(elem)
            yield tuple(result)
    
  • したがっお、この関数は任意の数のiterableオブゞェクトを取り蟌み、それらに察しお next 関数を呌び出し、 result リストに各アむテムを远加し、いずれかのiterableが尜きるず停止したす。

  • ここでの泚意点は、任意のiterableが尜きた堎合、result リストの既存の芁玠が砎棄されるこずです。numbers_iter の 3 にもこれが起きたした。

  • 䞊蚘の凊理を zip を䜿っお正しく行う方法は以䞋の通りです。

    >>> numbers = list(range(7))
    >>> numbers_iter = iter(numbers)
    >>> list(zip(first_three, numbers_iter))
    [(0, 0), (1, 1), (2, 2)]
    >>> list(zip(remaining, numbers_iter))
    [(3, 3), (4, 4), (5, 5), (6, 6)]
    
    
  • zipの最初の匕数は、最も少ない芁玠を持぀ものであるべきです。


▶ ルヌプ倉数が挏れる

for x in range(7):
    if x == 6:
        print(x, ': for x inside loop')
print(x, ': x in global')

出力:

6 : for x inside loop
6 : x in global

x はforルヌプのスコヌプの倖では定矩されおいたせんでした 

# 今床は最初にxを初期化しおみたす
x = -1
for x in range(7):
    if x == 6:
        print(x, ': for x inside loop')
print(x, ': x in global')

出力:

6 : for x inside loop
6 : x in global

出力 (Python 2.x):

>>> x = 1
>>> print([x for x in range(5)])
[0, 1, 2, 3, 4]
>>> print(x)
4

出力 (Python 3.x):

>>> x = 1
>>> print([x for x in range(5)])
[0, 1, 2, 3, 4]
>>> print(x)
1

💡 解説:

  • Pythonでは、forルヌプは存圚するスコヌプを䜿甚し、定矩されたルヌプ倉数を埌に残したす。これは、明瀺的にforルヌプ倉数をグロヌバル名前空間で定矩した堎合にも適甚されたす。この堎合、既存の倉数に再バむンドしたす。

  • リスト内包衚蚘の䟋におけるPython 2.xずPython 3.xのむンタヌプリタヌの出力の違いは、What’s New In Python 3.0の倉曎履歎にお解説されおいたす。

    "リスト内包衚蚘はもう [... for var in item1, item2, ...] の構文圢匏をサポヌトしおいたせん。代わりに [... for var in (item1, item2, ...)] をご䜿甚ください。たた、リスト内包衚蚘は異なるセマンティクスを持っおいるこずにご泚意ください。list() コンストラクタ内のゞェネレヌタ匏に察する構文糖に近いもので、特にルヌプ制埡倉数はもはや呚囲のスコヌプには挏れたせん。"


デフォルトの可倉匕数に泚意

def some_func(default_arg=[]):
    default_arg.append("some_string")
    return default_arg

出力:

>>> some_func()
['some_string']
>>> some_func()
['some_string', 'some_string']
>>> some_func([])
['some_string']
>>> some_func()
['some_string', 'some_string', 'some_string']

💡 解説:

Python関数のデフォルトの可倉匕数は、関数を呌び出すたびに本圓に初期化されるわけではありたせん。代わりに、最近割り圓おられた倀がデフォルト倀ずしお䜿甚されたす。明瀺的に[]をsome_funcに匕数ずしお枡した堎合、default_arg倉数のデフォルト倀は䜿甚されないため、関数は期埅通りの結果を返したす。

def some_func(default_arg=[]):
    default_arg.append("some_string")
    return default_arg

出力:

>>> some_func.__defaults__ # これは関数のデフォルト匕数倀を瀺したす
([],)
>>> some_func()
>>> some_func.__defaults__
(['some_string'],)
>>> some_func()
>>> some_func.__defaults__
(['some_string', 'some_string'],)
>>> some_func([])
>>> some_func.__defaults__
(['some_string', 'some_string'],)

可倉匕数によるバグを避けるための䞀般的な方法は、デフォルト倀ずしおNoneを割り圓お、埌でその匕数に察しお倀が関数に枡されたかどうかをチェックするこずです。䟋:

def some_func(default_arg=None):
    if default_arg is None:
        default_arg = []
    default_arg.append("some_string")
    return default_arg

▶ 䟋倖凊理の萜ずし穎

some_list = [1, 2, 3]
try:
    # これは ``IndexError`` を発生させるはずです
    print(some_list[4])
except IndexError, ValueError:
    print("Caught!")

try:
    # これは ``ValueError`` を発生させるはずです
    some_list.remove(4)
except IndexError, ValueError:
    print("Caught again!")

出力 (Python 2.x):

Caught!

ValueError: list.remove(x): x not in list

出力 (Python 3.x):

  File "<input>", line 3
    except IndexError, ValueError:
                     ^
SyntaxError: invalid syntax

💡 解説:

耇数の䟋倖をexcept節に远加するには、最初の匕数ずしお䞞括匧で囲ったタプルを枡す必芁がありたす。2番目の匕数はオプションで、提䟛された堎合には発生した䟋倖むンスタンスにバむンドされたす。䟋、

some_list = [1, 2, 3]
try:
   # これは ``ValueError`` を発生させるはずです
   some_list.remove(4)
except (IndexError, ValueError), e:
   print("Caught again!")
   print(e)

出力 (Python 2.x):

Caught again!
list.remove(x): x not in list

出力 (Python 3.x):

  File "<input>", line 4
    except (IndexError, ValueError), e:
                                     ^
IndentationError: unindent does not match any outer indentation level

䟋倖ず倉数をコンマで区切るこずは非掚奚であり、Python 3では機胜したせん。正しくはasを䜿甚したす。䟋、

some_list = [1, 2, 3]
try:
    some_list.remove(4)

except (IndexError, ValueError) as e:
    print("Caught again!")
    print(e)

出力

Caught again!
list.remove(x): x not in list

▶ 同じオペランド、異なる結果

a = [1, 2, 3, 4]
b = a
a = a + [5, 6, 7, 8]

出力:

>>> a
[1, 2, 3, 4, 5, 6, 7, 8]
>>> b
[1, 2, 3, 4]
a = [1, 2, 3, 4]
b = a
a += [5, 6, 7, 8]

出力:

>>> a
[1, 2, 3, 4, 5, 6, 7, 8]
>>> b
[1, 2, 3, 4, 5, 6, 7, 8]

💡 解説:

  • a += b は垞に a = a + b ず同じようには動䜜したせん。クラスは op= 挔算子を異なる方法で実装するこずもでき、リストはそうしたす。
  • 匏 a = a + [5,6,7,8] は新しいリストを生成し、aの参照をその新しいリストに蚭定したすが、bは倉曎されたせん。
  • 匏 a += [5,6,7,8] は実際には "extend" 関数にマップされ、その関数はリストに察しお操䜜されるので、a ず b は同じリストを指し続けたすが、そのリストはその堎で倉曎されたす。

▶ クラススコヌプを無芖する名前解決

x = 5
class SomeClass:
    x = 17
    y = (x for i in range(10))

出力:

>>> list(SomeClass.y)[0]
5
x = 5
class SomeClass:
    x = 17
    y = [x for i in range(10)]

出力 (Python 2.x):

>>> SomeClass.y[0]
17

出力 (Python 3.x):

>>> SomeClass.y[0]
5

💡 解説:

  • クラス定矩内にネストされたスコヌプは、クラスレベルでバむンドされた名前を無芖したす。
  • ゞェネレヌタ匏には独自のスコヌプがありたす。
  • Python 3.Xから、リスト内包衚蚘も独自のスコヌプを持ちたす。

▶ 「銀行家の䞞め」

リストの䞭倮の芁玠を取埗する簡単な関数を実装しおみたしょう

def get_middle(some_list):
    mid_index = round(len(some_list) / 2)
    return some_list[mid_index - 1]

Python 3.x:

>>> get_middle([1])  # よし
1
>>> get_middle([1,2,3])  # よし
2
>>> get_middle([1,2,3,4,5])  # え
2
>>> len([1,2,3,4,5]) / 2  # よし
2.5
>>> round(len([1,2,3,4,5]) / 2)  # なんで
2

Pythonによるず2.5の四捚五入は2のようです。

💡 解説:

  • これは浮動小数点の粟床の問題ではなく、意図的な挙動なのです。Python 3.0から、round()は銀行家の䞞めを䜿甚しおおり、0.5の端数は最も近い偶数に䞞められたす
>>> round(0.5)
0
>>> round(1.5)
2
>>> round(2.5)
2
>>> import numpy  # numpyも同じ結果を返したす>>> numpy.round(0.5)
0.0
>>> numpy.round(1.5)
2.0
>>> numpy.round(2.5)
2.0
  • これは、数倀の端数を四捚五入する際に掚奚される方法で、IEEE 754にも蚘茉されおいたす。ただし、これは通垞孊校で教えられる方法れロから離れる方向ぞの四捚五入ずは異なるため、「銀行家の䞞め」はあたり䞀般的ではありたせん。実際、倚くの人気のプログラミング蚀語JavaScript、Java、C/C++、Ruby、Rustなどでは「銀行家の䞞め」は䜿甚されおいたせん。そのため、Python独特の特城ずしお、端数の四捚五入に関しおの混乱を招くこずがありたす。
  • 詳しくは、round()関数のドキュメントやこのStackOverflowのスレッドをご参照ください。
  • get_middle([1]) が1を返したのは、むンデックスが round(0.5) - 1 = 0 - 1 = -1 ず蚈算され、リストの最埌の芁玠が返されたためです。

▶ 藁の䞭の針

経隓豊富なPythonプログラマヌで、以䞋のシナリオの1぀以䞊に遭遇しおいないずいう人は䞀人もいないでしょう。

x, y = (0, 1) if True else None, None

出力:

>>> x, y  # (0, 1)が期埅される
((0, 1), None)
t = ('one', 'two')
for i in t:
    print(i)

t = ('one')
for i in t:
    print(i)

t = ()
print(t)

出力:

one
two
o
n
e
tuple()
ten_words_list = [
    "some",
    "very",
    "big",
    "list",
    "that"
    "consists",
    "of",
    "exactly",
    "ten",
    "words"
]

出力

>>> len(ten_words_list)
9
a = "python"
b = "javascript"

出力:

# アサヌション倱敗メッセヌゞを持぀assert文
>>> assert(a == b, "Both languages are different")
# AssertionErrorは発生しない
some_list = [1, 2, 3]
some_dict = {
  "key_1": 1,
  "key_2": 2,
  "key_3": 3
}

some_list = some_list.append(4)
some_dict = some_dict.update({"key_4": 4})

出力:

>>> print(some_list)
None
>>> print(some_dict)
None
def some_recursive_func(a):
    if a[0] == 0:
        return
    a[0] -= 1
    some_recursive_func(a)
    return a

def similar_recursive_func(a):
    if a == 0:
        return a
    a -= 1
    similar_recursive_func(a)
    return a

出力:

>>> some_recursive_func([5, 0])
[0, 0]
>>> similar_recursive_func(5)
4

💡 解説:

  • 1に぀いお、期埅される動䜜のための正しい文はx, y = (0, 1) if True else (None, None)です。

  • 2に぀いお、期埅される動䜜のための正しい文はt = ('one',)たたはt = 'one',コンマが抜けおいたす。そうでない堎合、むンタプリタはtをstrずみなし、文字ごずにむテレヌトしたす。

  • ()は特別なトヌクンで、空のtupleを衚したす。

  • 3では、おそらくすでに気づかれたかず思いたすが、リストの5番目の芁玠"that"の埌にコンマが抜けおいたす。そのため、暗黙の文字列リテラル連結によっお、

    >>> ten_words_list
    ['some', 'very', 'big', 'list', 'thatconsists', 'of', 'exactly', 'ten', 'words']
    

ずなりたす。

4番目のスニペットではAssertionErrorは発生したせんでした。なぜなら、個々の匏a == bを䞻匵するのではなく、党䜓のタプルを䞻匵しおいるからです。以䞋のスニペットだずさらに分かりやすいでしょう。

>>> a = "python"
>>> b = "javascript"
>>> assert a == b
Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
AssertionError

>>> assert (a == b, "Values are not equal")
<stdin>:1: SyntaxWarning: assertion is always true, perhaps remove parentheses?

>>> assert a == b, "Values are not equal"
Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
AssertionError: Values are not equal
  • 5番目のスニペットに関しおは、list.append、dict.update、list.sortなどのようにシヌケンスやマッピングオブゞェクトのアむテムを倉曎するほずんどのメ゜ッドは、オブゞェクトをその堎で倉曎し、Noneを返したす。これの背埌にある理由は、操䜜がその堎で行える堎合にオブゞェクトのコピヌを䜜成するこずなくパフォヌマンスを向䞊させるためですこちらから匕甚。
  • 最埌の䟋は明らかでしょうが、倉曎可胜なオブゞェクトリストなどは関数内で倉曎可胜であり、むミュヌタブルな再代入a -= 1は倀の倉曎ではありたせん。

これらの现かい点に気を぀けるこずで、長期的にデバッグの劎力を倧幅に節玄できたす。


▶ Splitsies

>>> 'a'.split()
['a']

# 以䞋ず同じ
>>> 'a'.split(' ')
['a']

# しかし
>>> len(''.split())
0

# 以䞋ず同じではない
>>> len(''.split(' '))
1

💡 解説:

  • 最初は、splitのデフォルト区切り文字が単䞀スペヌス ' ' であるず考えるかもしれたせんが、ドキュメントによるず、

    sep が指定されおいないか None の堎合、異なる分割アルゎリズムが適甚されたす。連続する空癜は単䞀の区切り文字ずみなされ、文字列が先頭たたは末尟に空癜を含んでいる堎合、結果には空の文字列が含たれたせん。そのため、空の文字列や空癜のみからなる文字列を None で分割するず [] が返されたす。
    sep が䞎えられた堎合、連続する区切り文字は䞀緒にグルヌプ化されず、空の文字列を区切るものずみなされたす䟋えば、'1,,2'.split(',') は ['1', '', '2'] を返したす。指定された区切り文字で空の文字列を分割するず [''] が返されたす。

  • 以䞋の䟋で先頭ず末尟の空癜がどのように扱われるかを泚意深く芋るず、より分かりやすいです。

>>> ' a '.split(' ')
['', 'a', '']
>>> ' a '.split()
['a']
>>> ''.split(' ')
['']

▶ ワむルドむンポヌト

# ファむル: module.py

def some_weird_name_func_():
    print("works!")

def _another_weird_name_func():
    print("works!")

出力

>>> from module import *
>>> some_weird_name_func_()
"works!"
>>> _another_weird_name_func()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name '_another_weird_name_func' is not defined

💡 解説:

  • ワむルドカヌドむンポヌトを䜿甚しないこずはよく掚奚されたす。その最も明癜な理由の䞀぀は、ワむルドカヌドむンポヌトでは、アンダヌスコアで始たる名前がむンポヌトされないこずです。これによりランタむム䞭に゚ラヌが発生する可胜性がありたす。

  • from ... import a, b, c の構文を䜿甚しおいた堎合、䞊蚘の NameError は発生したせんでした。

    >>> from module import some_weird_name_func_, _another_weird_name_func
    >>> _another_weird_name_func()
    works!
    

どうしおもワむルドカヌドむンポヌトを䜿甚したい堎合は、__all__ リストをモゞュヌル内で定矩する必芁がありたす。これはワむルドカヌドむンポヌトを行った際に利甚可胜ずなる公開オブゞェクトのリストを含むものです。

__all__ = ['_another_weird_name_func']

def some_weird_name_func_():
    print("works!")

def _another_weird_name_func():
    print("works!")

出力

>>> _another_weird_name_func()
"works!"
>>> some_weird_name_func_()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'some_weird_name_func_' is not defined

▶ すべおsorted

>>> x = 7, 8, 9>>> sorted(x) == x
False
>>> sorted(x) == sorted(x)
True

>>> y = reversed(x)
>>> sorted(y) == sorted(y)
False

💡 解説:

  • sorted メ゜ッドは垞にリストを返し、リストずタプルを比范するずPythonでは垞に False を返したす。
>>> [] == tuple()
False
>>> x = 7, 8, 9
>>> type(x), type(sorted(x))
(tuple, list)
  • sorted ずは異なり、reversed メ゜ッドはむテレヌタを返したす。なぜなら、゜ヌトにはむテレヌタをその堎で倉曎するか、远加のコンテナリストを䜿甚する必芁があるのに察し、逆順には単に最埌のむンデックスから最初のむンデックスに向かっおむテレヌトするだけで枈むからです。
  • したがっお sorted(y) == sorted(y) の比范では、最初の sorted() の呌び出しでむテレヌタ y が消費され、次の呌び出しでは空のリストが返されたす。
>>> x = 7, 8, 9
>>> y = reversed(x)
>>> sorted(y), sorted(y)
([7, 8, 9], [])

▶ 真倜䞭は存圚しない

from datetime import datetime

midnight = datetime(2018, 1, 1, 0, 0)
midnight_time = midnight.time()

noon = datetime(2018, 1, 1, 12, 0)
noon_time = noon.time()

if midnight_time:
    print("Time at midnight is", midnight_time)

if noon_time:
    print("Time at noon is", noon_time)

出力 (< 3.5):

('Time at noon is', datetime.time(12, 0))

真倜䞭の時間Time at midnightが衚瀺されおいたせん。

💡 解説:

Python 3.5より前では、datetime.time オブゞェクトがUTCで真倜䞭を衚しおいる堎合、そのブヌル倀は False ずみなされおいたした。if obj: 構文を䜿甚しお obj が None たたは「空」の同等物かどうかをチェックする堎合、゚ラヌが発生しやすいです。


セクション:隠された宝物たち

このセクションでは、ほずんどの初心者が知らない、Pythonに関する興味深い特城を取り䞊げおいきたす。

▶ Python、空を飛びたいんだけど 

では、これを詊しおみおください。

import antigravity

出力:
しヌ 秘密ですよ。

💡 解説:

  • antigravity モゞュヌルはPython開発者によっおリリヌスされたいく぀かのむヌスタヌ゚ッグの内の䞀぀です。
  • import antigravity を実行するず、Pythonに぀いおのクラシックなXKCDコミックを衚瀺するりェブブラりザが開きたす。
  • それだけではありたせん。実は、むヌスタヌ゚ッグの䞭の別のむヌスタヌ゚ッグもありたす。コヌドを芋るず、XKCDのゞオハッシングアルゎリズムを実装するず䞻匵する関数が定矩されおいたす。

▶ goto、でもなぜ

from goto import goto, label
for i in range(9):
    for j in range(9):
        for k in range(9):
            print("I am trapped, please rescue!")
            if k == 2:
                goto .breakout # 深くネストされたルヌプから抜け出す
label .breakout
print("Freedom!")

出力 (Python 2.3):

I am trapped, please rescue!
I am trapped, please rescue!
Freedom!

💡 解説:

  • goto の動䜜するバヌゞョンは、2004幎4月1日の゚むプリルフヌルのゞョヌクずしお発衚されたした。
  • 珟圚のPythonバヌゞョンにはこのモゞュヌルは含たれおいたせん。
  • 動䜜はしたすが、䜿甚しないでください。Pythonに goto がない理由はこちら。

▶ カッコ぀けよう

Pythonでスコヌプを瀺すために空癜を䜿うのが奜きでない人は、以䞋をむンポヌトするこずでC蚀語スタむルの {} を䜿うこずができたす。

from __future__ import braces

出力:

  File "some_file.py", line 1
    from __future__ import braces
SyntaxError: not a chance

「あれブレヌスは」ず、倱望された方はJavaをお䜿いください。さお、もう䞀぀驚くべきこずですが、__future__ モゞュヌルのコヌドにおいお SyntaxError がどこで発生するか芋぀けられたすか

💡 解説:

  • __future__ モゞュヌルは通垞、Pythonの将来のバヌゞョンからの機胜を提䟛するために䜿甚されたす。しかし、この特定のコンテキストにおける "future" は、皮肉です。
  • これは、この問題に関するコミュニティの感情に関するむヌスタヌ゚ッグです。
  • 実際のコヌドは future.c ファむルのここに存圚したす。
  • CPythonコンパむラが future ステヌトメントに遭遇するず、通垞のむンポヌトステヌトメントずしお扱う前に future.c の適切なコヌドを最初に実行したす。

▶ Barryおじさん

出力 (Python 3.x)

>>> from __future__ import barry_as_FLUFL
>>> "Ruby" != "Python" # これは疑う䜙地がありたせん
  File "some_file.py", line 1
    "Ruby" != "Python"
              ^
SyntaxError: invalid syntax

>>> "Ruby" <> "Python"
True

それでは行きたしょう。

💡 解説:

  • これは、2009幎4月1日に発衚されたPEP-401に関連しおいたす今なら、どういう意味かお分かりでしょう。

  • PEP-401からの匕甚

    Python 3.0における != 䞍等号挔算子はひどい間違いであり、指の痛みを誘発するものであったこずが認識されたため、FLUFLは <> ダむアモンド挔算子を唯䞀の綎りずしお再導入する。

  • Uncle BarryはPEPでさらに倚くのこずを共有しおおり、こちらで読むこずができたす。

  • 察話的環境ではうたく機胜したすが、Pythonファむル経由で実行するず SyntaxError が発生したすこちらを参照。しかし、eval や compile 内でステヌトメントをラップするこずで機胜させるこずができたす。

    from __future__ import barry_as_FLUFL
    print(eval('"Ruby" <> "Python"'))
    

▶ Pythonでも知っおいるこず愛は耇雑

import this

お、これthisは䜕だthis is love ❀

出力:

The Zen of Python, by Tim Peters

Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea -- let's do more of those!

Zen of Pythonです

>>> love = this
>>> this is love
True
>>> love is True
False
>>> love is False
False
>>> love is not True or False
True
>>> love is not True or False; love is love  # 愛は耇雑だ
True

💡解説:

  • Pythonの this モゞュヌルは、The Zen of PythonPEP 20のむヌスタヌ゚ッグです。
  • 興味のある方は、this.pyの実装をご確認ください。面癜いこずに、犅のコヌドは自身に違反しおいたすおそらく唯䞀のケヌスです。
  • love is not True or False; love is love ずいう文に関しおは、皮肉なこずに自明ですもし分からないなら、is ず is not 挔算子に関する䟋をご芧ください。

▶ 実は存圚するんです

ルヌプのための else 節。 以䞋は兞型的な䟋です

  def does_exists_num(l, to_find):
      for num in l:
          if num == to_find:
              print("存圚したす")
              break
      else:
          print("存圚したせん")

出力:

>>> some_list = [1, 2, 3, 4, 5]
>>> does_exists_num(some_list, 4)
Exists!
>>> does_exists_num(some_list, -1)
Does not exist

䟋倖凊理における else 節。 䟋えば、

try:
    pass
except:
    print("Exception occurred!!!")
else:
    print("Try block executed successfully...")

出力:

Try block executed successfully...

💡 解説:

  • ルヌプの埌の else 節は、すべおの繰り返しの埌に明瀺的な break がない堎合にのみ実行されたす。"nobreak" 節ず考えるこずができたす。
  • try ブロックの埌の else 節は、「完了節」ずも呌ばれたす。なぜなら try 文の else 節に到達するずいうこずは、tryブロックが実際に正垞に完了したこずを意味するからです。

▶ Ellipsis

def some_func():
    Ellipsis

出力

>>> some_func()
# 出力なし、゚ラヌなし

>>> SomeRandomString
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'SomeRandomString' is not defined

>>> Ellipsis
Ellipsis

💡 解説:

  • Pythonでは、Ellipsisはグロヌバルに利甚可胜な組み蟌みオブゞェクトであり、...ず同等です。

    >>> ...
    Ellipsis
    

省略蚘号はいく぀かの目的で䜿甚できたす。

  • ただ曞かれおいないコヌドのプレヌスホルダヌずしおpass文ず同様
  • スラむス構文で、残りの方向の完党なスラむスを衚すために䜿甚
>>> import numpy as np
>>> three_dimensional_array = np.arange(8).reshape(2, 2, 2)
array([
    [
        [0, 1],
        [2, 3]
    ],

    [
        [4, 5],
        [6, 7]
    ]
])

3次元配列 three_dimensional_array は配列の配列の配列です。内郚の配列のすべおの第2芁玠むンデックス 1を出力したい堎合は、省略蚘号を䜿甚しお前にある次元をバむパスできたす。

>>> three_dimensional_array[:,:,1]
array([[1, 3],
   [5, 7]])
>>> three_dimensional_array[..., 1] # 省略蚘号を䜿甚。
array([[1, 3],
   [5, 7]])
  • これは任意の次元数で機胜したす。最初ず最埌の次元でスラむスを遞択し、䞭間の次元を無芖するこずもできたすn_dimensional_array[firs_dim_slice, ..., last_dim_slice]。
    • 型ヒントで型の䞀郚だけを瀺すために䜿甚䟋(Callable[..., int] や Tuple[str, ...]。
    • 関数のデフォルト匕数ずしお省略蚘号を䜿甚するこずもできたす"匕数が枡されない"ず"None倀が枡される"のシナリオを区別したい堎合。

▶ むンピニティ

打ち間違えではありたせん。

出力 (Python 3.x):

>>> infinity = float('infinity')
>>> hash(infinity)
314159
>>> hash(float('-inf'))
-314159

💡 解説:

  • 無限のハッシュは10⁵ x πです。
  • 面癜いこずに、Python 3では float('-inf') のハッシュは "-10⁵ x π" ですが、Python 2では "-10⁵ x e" です。

▶ マングリングしよう

class Yo(object):
    def __init__(self):
        self.__honey = True
        self.bro = True

出力:

>>> Yo().bro
True
>>> Yo().__honey
AttributeError: 'Yo' object has no attribute '__honey'
>>> Yo()._Yo__honey
True

class Yo(object):
    def __init__(self):
        # 今床は察称的なものを詊しおみたしょう
        self.__honey__ = True
        self.bro = True

出力:

>>> Yo().bro
True

>>> Yo()._Yo__honey__
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Yo' object has no attribute '_Yo__honey__'

なぜ Yo()._Yo__honey は機胜したのでしょうか

_A__variable = "Some value"

class A(object):
    def some_func(self):
        return __variable # ただどこにも初期化されおいなたせん

出力:

>>> A().__variable
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'A' object has no attribute '__variable'

>>> A().some_func()
'Some value'

💡 解説:

  • 名前のマングリングは、異なる名前空間間の呜名衝突を避けるために䜿甚されたす。
  • Pythonでは、__ダブルアンダヌスコア、"dunder"ずも呌ばれるで始たり、䞀぀以䞊の末尟アンダヌスコアで終わらないクラスメンバヌ名は、クラス名の前に _NameOfTheClass を远加するこずで修正マングリングされたす。
  • したがっお、最初のスニペットで __honey 属性にアクセスするには、前に _Yo を远加する必芁がありたした。これにより、他のクラスで定矩された同じ名前の属性ずの衝突を防ぐこずができたす。
  • では、なぜ2番目のスニペットでは機胜しなかったのでしょうかそれは名前のマングリングが二重アンダヌスコアで終わる名前を陀倖するからです。
  • 3番目のスニペットも名前のマングリングの結果です。return __variable のステヌトメント内の名前 __variable は _A__variable にマングリングされ、これは倖郚スコヌプで宣蚀した倉数の名前ず同じです。
  • たた、マングリングされた名前が255文字を超える堎合は、切り捚おが行われたす。

セクション:芋た目に惑わされるな

▶ 行をスキップ

出力:

>>> value = 11
>>> valuе = 32
>>> value
11

えっ

泚意: これを再珟する最も簡単な方法は、䞊蚘のスニペットからステヌトメントをコピヌしおファむル/シェルに貌り付けるこずです。

💡 解説:

䞀郚の非西掋文字は英字アルファベットの文字ず同䞀に芋えたすが、むンタヌプリタヌによっお異なるものず芋なされたす。

>>> ord('е') # キリル文字の 'e' (Ye)
1077
>>> ord('e') # ラテン文字の 'e'、英語で䜿甚され、暙準キヌボヌドで入力
101
>>> 'е' == 'e'
False

>>> value = 42 # ラテン文字の e>>> valuе = 23 # キリル文字の 'e', Python 2.x むンタヌプリタヌはここで `SyntaxError` を発生させる>>> value
42

組み蟌みの ord() 関数は文字のUnicode コヌドポむントを返し、キリル文字の 'e' ずラテン文字の 'e' の異なるコヌド䜍眮が䞊蚘の䟋の振る舞いを正圓化したす。


▶ テレポヌテヌション

# `pip install numpy` を先に実行
import numpy as np

def energy_send(x):
    # numpy配列を初期化
    np.array([float(x)])

def energy_receive():
    # 空のnumpy配列を返す
    return np.empty((), dtype=np.float).tolist()

出力:

>>> energy_send(123.456)
>>> energy_receive()
123.456

ノヌベル賞はどこぞ

💡 解説:

  • energy_send 関数で䜜成されたnumpy配列が返されないこずにご泚意ください。そのため、そのメモリスペヌスは再割り圓お可胜です。
  • numpy.empty() は初期化せずに次の空きメモリスロットを返したす。このメモリ䜍眮はたたたたちょうど解攟されたものず同じです通垞はそうですが、必ずしもそうではありたせん。

▶ 䜕かがおかしい 

def square(x):
    """
    A simple function to calculate the square of a number by addition.
    """
    sum_so_far = 0
    for counter in range(x):
        sum_so_far = sum_so_far + x
  return sum_so_far

出力 (Python 2.x):

>>> square(10)
10

なぜ100ではないのでしょうか

泚: 再珟できない堎合は、シェル経由でファむル mixed_tabs_and_spaces.py を実行しおみおください。

💡 解説:

  • タブずスペヌスを混圚させないでください return の盎前の文字は "タブ" で、䟋の他の堎所では "4スペヌス" の倍数でむンデントされおいたす。

  • Pythonがタブをどのように扱うかは以䞋の通りです。

    たず、タブは巊から右ぞず1から8のスペヌスに眮き換えられ、眮き換えを含めた総文字数が8の倍数ずなるようにされたす<...>

  • したがっお、square 関数の最埌の行の "タブ" は8぀のスペヌスに眮き換えられ、ルヌプに入り蟌みたす。

  • Python 3は、このような堎合に自動的に゚ラヌを投げおくれるので芪切です。

    出力 (Python 3.x):

    TabError: inconsistent use of tabs and spaces in indentation
    

セクション:さたざたなトピック

▶ += の方が速い

# "+", 䞉぀の文字列を䜿っお:
>>> timeit.timeit("s1 = s1 + s2 + s3", setup="s1 = ' ' * 100000; s2 = ' ' * 100000; s3 = ' ' * 100000", number=100)
0.25748300552368164
# "+=", 䞉぀の文字列を䜿っお:
>>> timeit.timeit("s1 += s2 + s3", setup="s1 = ' ' * 100000; s2 = ' ' * 100000; s3 = ' ' * 100000", number=100)
0.012188911437988281

💡 解説:

  • 二぀以䞊の文字列を連結する堎合、+= は + よりも速いです。なぜなら最初の文字列s1 += s2 + s3 の堎合の s1 などが完党な文字列を蚈算する間に砎壊されないからです。

▶ 巚倧な文字列の䜜成

def add_string_with_plus(iters):
    s = ""
    for i in range(iters):
        s += "xyz"
    assert len(s) == 3*iters

def add_bytes_with_plus(iters):
    s = b""
    for i in range(iters):
        s += b"xyz"
    assert len(s) == 3*iters

def add_string_with_format(iters):
    fs = "{}"*iters
    s = fs.format(*(["xyz"]*iters))
    assert len(s) == 3*iters

def add_string_with_join(iters):
    l = []
    for i in range(iters):
        l.append("xyz")
    s = "".join(l)
    assert len(s) == 3*iters

def convert_list_to_string(l, iters):
    s = "".join(l)
    assert len(s) == 3*iters

出力:

# 結果の読みやすさのために ipython シェルで %timeit を䜿甚しお実行されたした。
# 暙準の python シェル/スクリプトでは timeit モゞュヌルを䜿甚するこずもできたす。䜿甚䟋は以䞋の通りです。
# timeit.timeit('add_string_with_plus(10000)', number=1000, globals=globals())

>>> NUM_ITERS = 1000
>>> %timeit -n1000 add_string_with_plus(NUM_ITERS)
124 µs ± 4.73 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
>>> %timeit -n1000 add_bytes_with_plus(NUM_ITERS)
211 µs ± 10.5 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
>>> %timeit -n1000 add_string_with_format(NUM_ITERS)
61 µs ± 2.18 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
>>> %timeit -n1000 add_string_with_join(NUM_ITERS)
117 µs ± 3.21 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
>>> l = ["xyz"]*NUM_ITERS
>>> %timeit -n1000 convert_list_to_string(l, NUM_ITERS)
10.1 µs ± 1.06 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

むテレヌション回数を10倍に増やしおみたしょう。

>>> NUM_ITERS = 10000>>> %timeit -n1000 add_string_with_plus(NUM_ITERS) # 実行時間が線圢に増加
1.26 ms ± 76.8 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
>>> %timeit -n1000 add_bytes_with_plus(NUM_ITERS) # 二次的に増加
6.82 ms ± 134 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
>>> %timeit -n1000 add_string_with_format(NUM_ITERS) # 線圢に増加
645 µs ± 24.5 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
>>> %timeit -n1000 add_string_with_join(NUM_ITERS) # 線圢に増加
1.17 ms ± 7.25 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
>>> l = ["xyz"]*NUM_ITERS
>>> %timeit -n1000 convert_list_to_string(l, NUM_ITERS) # 線圢に増加
86.3 µs ± 2 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

💡 解説:

  • timeitや%timeitに぀いおは、リンク先で詳しい解説を読むこずができたす。これらはコヌドの実行時間を枬定するために䜿甚されたす。

  • 長い文字列の生成には + を䜿甚しないでください — Pythonでは、str は䞍倉なので、2぀の文字列を連結するたびに、巊右の文字列が新しい文字列にコピヌされなければなりたせん。10文字の4぀の文字列を連結する堎合、40文字ではなく90文字(10+10) + ((10+10)+10) + (((10+10)+10)+10)をコピヌするこずになりたす。文字列の数ずサむズが増えるに぀れお、状況は二次的に悪化したすadd_bytes_with_plus 関数の実行時間で正圓化されおいたす。

  • そのため、.format. たたは % 構文の䜿甚が掚奚されたすただし、非垞に短い文字列の堎合は + よりもわずかに遅いです。

  • さらに良い方法は、既に内容がiterableオブゞェクトの圢で利甚可胜であれば、''.join(iterable_object)を䜿甚するこずです。これは圧倒的に速いです。

  • 前の䟋で議論された += の最適化のため、add_bytes_with_plus ずは異なり、add_string_with_plus は実行時間の二次的な増加を瀺したせんでした。s += "xyz" の代わりに s = s + "x" + "y" + "z" であった堎合、増加は二次的になりたす。

    def add_string_with_plus(iters): s = "" for i in range(iters): s = s + "x" + "y" + "z" assert len(s) == 3*iters
    >>> %timeit -n100 add_string_with_plus(1000)
    388 µs ± 22.4 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
    >>> %timeit -n100 add_string_with_plus(10000) # 実行時間の二次的な増加
    9 ms ± 298 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
    
  • 巚倧な文字列を圢成し、フォヌマットの方法が倚すぎるのは、Zen of Pythonず察立しおいるようでもありたす。以䞋匕甚。

    それを行う方法は䞀぀だけであり、できればただ䞀぀の明癜な方法があるべきです。


▶ dict怜玢の遅延

some_dict = {str(i): 1 for i in range(1_000_000)}
another_dict = {str(i): 1 for i in range(1_000_000)}

出力:

>>> %timeit some_dict['5']
28.6 ns ± 0.115 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)
>>> some_dict[1] = 1
>>> %timeit some_dict['5']
37.2 ns ± 0.265 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)

>>> %timeit another_dict['5']
28.5 ns ± 0.142 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)
>>> another_dict[1]  # Trying to access a key that doesn't exist
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
KeyError: 1
>>> %timeit another_dict['5']
38.5 ns ± 0.0913 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)

なぜ同じ怜玢が遅くなるのでしょうか

💡 解説:

  • CPythonには、すべおのキヌタむプstr、int、任意のオブゞェクト...を扱う汎甚の蟞曞怜玢関数ず、strのみから構成される蟞曞の䞀般的なケヌス甚の特化した関数がありたす。
  • 特化した関数CPythonの゜ヌスでは lookdict_unicode ず呜名されおいたすは、すべおのキヌ怜玢されるキヌを含むが文字列であるこずを知っおおり、__eq__ メ゜ッドを呌び出す代わりに、より高速か぀単玔な文字列比范を䜿甚しおキヌを比范したす。
  • dictむンスタンスが非strキヌでアクセスされるず、将来の怜玢で汎甚関数を䜿甚するように倉曎されたす。
  • このプロセスは特定のdictむンスタンスに察しおは元に戻すこずができず、キヌが蟞曞に存圚する必芁もありたせん。そのため、倱敗したルックアップを詊みるこずでも同じ効果がありたす。

▶ むンスタンスdictの膚匵

import sys

class SomeClass:
    def __init__(self):
        self.some_attr1 = 1
        self.some_attr2 = 2
        self.some_attr3 = 3
        self.some_attr4 = 4

def dict_size(o):
    return sys.getsizeof(o.__dict__)

出力: (Python 3.8、他のPython 3バヌゞョンは少し異なる堎合がありたす)

>>> o1 = SomeClass()
>>> o2 = SomeClass()
>>> dict_size(o1)
104
>>> dict_size(o2)
104
>>> del o1.some_attr1
>>> o3 = SomeClass()
>>> dict_size(o3)
232
>>> dict_size(o1)
2

もう䞀床詊しおみたしょう... 新しいむンタヌプリタヌで:

>>> o1 = SomeClass()
>>> o2 = SomeClass()
>>> dict_size(o1)
104  # 予想通り
>>> o1.some_attr5 = 5>>> o1.some_attr6 = 6>>> dict_size(o1)
360
>>> dict_size(o2)
272
>>> o3 = SomeClass()
>>> dict_size(o3)
232

これらの蟞曞が膚匵する原因は䜕でしょうかたた、新しく䜜成されたオブゞェクトも膚匵しおいるのはなぜでしょうか

💡 解説:

  • CPythonは耇数の蟞曞で同じ"キヌ"オブゞェクトを再利甚するこずができたす。これはPEP 412で远加され、特にむンスタンスの蟞曞でメモリ䜿甚量を削枛するこずを目的ずしおいたす - キヌむンスタンス属性はすべおのむンスタンスに共通する傟向がありたす。
  • この最適化はむンスタンス蟞曞に完党に透過的ですが、特定の前提が砎られるず無効になりたす。
  • キヌ共有蟞曞は削陀をサポヌトしおいたせん。むンスタンス属性が削陀されるず、蟞曞は「共有解陀」され、同じクラスの将来のすべおのむンスタンスに察しおキヌ共有が無効になりたす。
  • さらに、蟞曞のキヌがリサむズされた堎合新しいキヌが挿入されたため、それらは ただ䞀぀の蟞曞 によっお䜿甚されおいる堎合にのみ共有されたすこれにより、最初に䜜成されたむンスタンスの __init__ 内で倚くの属性を远加しおも「共有解陀」が発生せず、共有が継続されたす。耇数のむンスタンスが存圚する状態でリサむズが発生するず、同じクラスの将来のすべおのむンスタンスに察しおキヌ共有が無効になりたすCPythonはむンスタンスが同じ属性セットを䜿甚しおいるかどうかを刀断できなくなり、キヌの共有を詊みるこずを諊めたす。
  • プログラムのメモリフットプリントを䜎枛させたい堎合の小さなヒントむンスタンス属性を削陀しないでください、そしお __init__ 内ですべおの属性を初期化しおください

▶ その他の小さな発芋

  • join() はリスト操䜜ではなく文字列操䜜です。最初に䜿うずきは盎感に反するかもしれたせん

    💡 説明: join() が文字列のメ゜ッドなら、任意のむテラブルリスト、タプル、むテレヌタに察しお操䜜を行うこずができたす。リストのメ゜ッドであれば、各タむプごずに別々に実装する必芁がありたす。たた、䞀般的な list オブゞェクトのAPIに文字列専甚のメ゜ッドを蚭けるこずはあたり意味がありたせん。

  • 奇劙に芋えるが意味的には正しいステヌトメント

    • [] = () は意味的に正しいステヌトメントです空の tuple を空の list に展開しおいたす
    • 'a'[0][0][0][0][0] も意味的には正しいです。Pythonには他のCから掟生した蚀語のようなキャラクタヌデヌタ型がないため、文字列から1文字を遞択するず1文字の文字列が返されたす。
    • 3 --0-- 5 == 8 ず 5 == 5 は䞡方ずも意味的に正しいステヌトメントで、結果は True になりたす。
  • a が数倀である堎合、++a ず a は有効なPythonステヌトメントですが、C、C++、Javaなどの蚀語での類䌌のステヌトメントずは同じような動䜜はしたせん。

    >>> a = 5
    >>> a
    5
    >>> ++a
    5
    >>> --a
    5
    
  • 💡 解説:

    • Pythonの文法には ++ 挔算子が存圚したせん。実際には2぀の + 挔算子がありたす。
    • ++a は +(+a) ずしお解析され、結果ずしお a になりたす。同様に、a のステヌトメントの出力も正圓化できたす。
    • Pythonにむンクリメントずデクリメント挔算子がない理由に぀いおはこのStackOverflow スレッドで議論されおいたす。
  • セむりチ挔算子に぀いおは知っおいたすね。しかし、スペヌスむンベヌダヌ挔算子に぀いおは聞いたこずがありたすか

    >>> a = 42
    >>> a -=- 1
    >>> a
    43
    

これは、別のむンクリメント挔算子ず䞀緒に䜿われる代替のむンクリメント挔算子です。

>>> a +=+ 1
>>> a
>>> 44
  • 💡 解説:

このゞョヌクは Raymond Hettingerのツむヌトからきおいたす。スペヌスむンベヌダヌ挔算子は実際には間違った圢匏の a -= (-1) です。これは a = a - (- 1) ず等䟡です。a += (+ 1) のケヌスも同様です。

  • Pythonには文曞化されおいない逆含意挔算子がありたす。

    >>> False ** False == True
    True
    >>> False ** True == False
    True
    >>> True ** False == True
    True
    >>> True ** True == True
    True
    
  • 💡 解説: False ず True を0ず1に眮き換えお蚈算するず、真理倀衚は逆含意挔算子ず等䟡になりたす。(出兞)

  • 挔算子の話題ですから、行列乗算のための @ 挔算子もありたす心配しないでください、今回は本物です。

    >>> import numpy as np
    >>> np.array([2, 2, 2]) @ np.array([7, 8, 8])
    46
    
  • 💡 解説: @ 挔算子は、科孊コミュニティを念頭に眮いおPython 3.5で远加されたした。任意のオブゞェクトは __matmul__ マゞックメ゜ッドをオヌバヌロヌドしおこの挔算子の動䜜を定矩するこずができたす。

  • Python 3.8以降では、f'{some_var=} のような兞型的なf文字列構文を䜿っお玠早くデバッグできたす。䟋えば、

    >>> some_string = "wtfpython">>> f'{some_string=}'
    "some_string='wtfpython'"
    

Pythonでは関数内のロヌカル倉数の栌玍に2バむトを䜿甚したす。理論的には、関数内に65536個の倉数しか定矩できないこずを意味したす。しかし、Pythonには2^16個以䞊の倉数名を栌玍するための䟿利な内蔵゜リュヌションがありたす。以䞋のコヌドは、65536個以䞊のロヌカル倉数が定矩されたずきにスタックで䜕が起こるかを瀺しおいたす譊告このコヌドは玄2^18行のテキストを出力するので、芚悟しおください

import dis
exec("""
def f():
   """ + """
   """.join(["X" + str(x) + "=" + str(x) for x in range(65539)]))

f()

print(dis.dis(f))
  • 耇数のPythonスレッドはPythonコヌドを同時に実行したせん聎き間違えではありたせんよ。耇数のスレッドを生成しおPythonコヌドを同時に実行するのは盎感的に思えるかもしれたせんが、PythonのGlobal Interpreter Lockのため、実際にはスレッドを同じコアで亀互に実行させおいるだけです。PythonのスレッドはIOバりンドタスクに適しおいたすが、CPUバりンドタスクで実際の䞊列化を達成するには、Pythonのmultiprocessingモゞュヌルを䜿甚するこずを怜蚎するかもしれたせん。

  • print メ゜ッドは倀をすぐに出力しないこずがありたす。䟋えば、

    # ファむル some_file.py
    import time
    
    print("wtfpython", end="_")
    time.sleep(3)
    
  • これは、end 匕数のために、出力バッファが \n に遭遇するか、プログラムの実行が終了するたでフラッシュされないため、wtfpython が3秒埌に出力されたす。バッファをフラッシュするためには flush=True 匕数を枡すこずができたす。

  • 範囲倖のむンデックスを䜿ったリストのスラむスぱラヌを投げたせん。

    >>> some_list = [1, 2, 3, 4, 5]
    >>> some_list[111:]
    []
    
  • iterableをスラむスするず垞に新しいオブゞェクトが䜜られるわけではありたせん。䟋えば、

    >>> some_str = "wtfpython"
    >>> some_list = ['w', 't', 'f', 'p', 'y', 't', 'h', 'o', 'n']
    >>> some_list is some_list[:] # 新しいオブゞェクトが䜜られるのでFalseが予想される
    False
    >>> some_str is some_str[:] # 文字列は䞍倉なので、新しいオブゞェクトを䜜るこずはあたり意味がないため、True。
    True
    
  • Python 3では、int('١٢٣ـ٥ي٧ٚ٩') は 123456789 を返したす。PythonにおけるDecimal characters十進数文字には、数字文字ず、十進数を圢成するために䜿える党おの文字が含たれたす。䟋えば、U+0660, ARABIC-INDIC DIGIT ZEROです。これに関連した面癜い話がありたす。

  • Python 3以降、数字リテラルはアンダヌスコア_を䜿っお区切るこずができたす読みやすさを向䞊させるため。

>>> six_million = 6_000_000
>>> six_million
6000000
>>> hex_address = 0xF00D_CAFE
>>> hex_address
4027435774
  • 'abc'.count('') == 4。ここに count メ゜ッドのおおよその実装があり、これが事情をより明確にしたす。

    def count(s, sub):
        result = 0
        for i in range(len(s) + 1 - len(sub)):
            result += (s[i:i + len(sub)] == sub)
        return result
    

・この挙動は、オリゞナルの文字列においお長さ0のスラむスず空の郚分文字列''が䞀臎するためです。

info-outline

お知らせ

K.DEVは株匏䌚瀟KDOTにより運営されおいたす。蚘事の内容や䌚瀟でのITに関わる䞀般的なご盞談に専門の瀟員がお答えしおおりたす。ぜひお気軜にご連絡ください。