見出し画像

サルでもわかる SettingWithCopyWarning 1/3 〜Python インデックス構文の代償〜

Python の、ちょっとだけニッチな話をします。その内容とは、pandas の SettingWithCopyWarning の解説です。ことしの春まで私が書いていた入門記事とは、対象読者がちょっと?変わります。


対象読者

  • 学業や仕事で pandas を使わざるを得ない方

  • SettingWithCopyWarning が ピンとこない方

  • 自分はサルではないかと疑い始めている方

はじめに

これから、SettingWithCopyWarning (長いので以下 SWCW と略します) の世の中で最も分かりやすい説明を目指します。いちど理解さえしてしまえば話はとても単純なはずなのですが、なかなか良い解説がないため、理解に苦しんでいる方は多いのではないでしょうか。私も相当理解に苦しみました。

ちなみに、これまででもっとも理解しやすいサイトは、日本語ではここだと思います。私もこのサイトを読んでようやくわかったので、原著者と翻訳者には感謝し、ここに敬意を表します。

ですが、もっと分かりやすく説明できないものかと思ったわけです。

サルにも分かる丁寧な解説を目指すため、記事はどうしても長くなり、3 回にわかれてしまいますが、ご了承ください。

なお pandas 3.0 から SWCW は 無くなります

ところで、現在開発中の pandas 3.0 からは、SWCW はついに無くなります。(かわりに、 CoW モード というものが強制され、ChainedAssignmentError というのが出るようになります。このエラーは SWCW ほど難解ではありません。「CoW モード」については追って解説します)

SWCW の消滅は 利用者にとって喜ばしいことですが、そのかわりに、3.0 から pandas は いったいどう変わるのでしょうか。それを理解するために、現時点で SWCW についてじっくり理解しても、損はないと思います。

まずは Python の言語仕様を解説

3回にわたる記事の1回目の今回は、SWCW の遠因となる、以下を確認します。

  • Pythonのオブジェクト指向プログラミング(OOP)言語としての側面

  • OOP言語としての側面を隠蔽する 「インデックス」 の仕様

結構な遠回りのようですが、急がば回れですのでおつきあいください。たんに データ分析をしたい方にとって、OOP なんてものまで理解しなければならないのは、そうとうに理不尽な話ではありますが、ちょっとのガマンです。

ポイントは大きく三つです。

  • オブジェクトへのメッセージ送信は OOP 言語でどう書かれるか

  • オブジェクトの名指しは OOP 言語でどう書かれるか

  • Python の「インデックス」は OOP 言語としての何を隠し、その代償は何か

そのくらいなら知っているよ~バカにすんなよ~そこまでのサルじゃないよ~という方は、今回1回目の記事は読み飛ばし、このあとの 2回目の記事から読み始めてください。

2回目の記事はこちらです。

メッセージ送信は OOP 言語でどう書かれるか

オブジェクト というのもそもそも分かりにくい言葉です。ここではまず、 オブジェクトとは 「メッセージ」をやりとりするものである、という漠とした、原始的なイメージにしたがって、そのイメージとソースコードの対比関係を追ってみましょう。

漠としたオブジェクト指向プログラミングのイメージ

これをコーディングするとき、Python や Java 言語では、メッセージの送信をメソッドとして表現します。

唐突に Java 言語 を登場させましたが、その理由は、Java 言語のほうが、この話題においては文法が素直で一貫性があるからです。(その代わりに、Java 言語 で書かれるコードは Python よりまどろっこしいです)。

映画「千と千尋の神隠し」のシーンを、Java 言語風のコードに起こしてみます。

// 今からお前の名前は千だ。いいかい、千だよ。
you.setName("千");

// 分かったら返事をするんだ、千!!
you.gotIt();  // は、はいっ!

// 名はなんという?
String yourName = you.getName();  // え?ち、…ぁ、千です。

.setName("千"), .gotIt(), .getName() というのが you というオブジェクトに対するメッセージです。これらはメソッドと呼ばれますね。また、オブジェクトによる応答メッセージが メソッドの戻り値で、これはコード上ではオブジェクト名とメソッドを合わせた全体として表現されます。

Java言語におけるメッセージの送信と応答

名指し は OOP言語でどう書かれるか

オブジェクトにメッセージを送信するコードを書くためには、そもそもオブジェクトを特定するための名指しが必要になります。オブジェクトを名指しするものが、変数です。先述の例では、you という変数が、オブジェクトを名指ししていました。

オブジェクトの名指しは、メッセージのやり取りとは、まったく異なる行いですね。イメージは、こんな感じです。

変数によるオブジェクトの名指し

これをコーディングするとき、Python や Java 言語 では、変数への代入 として記述します。Java 言語の書き方で見てみましょう。

// 名はなんという?
String yourName = you.getName(); // え?ち、…ぁ、千です。

// では千
Person sen = you;

// 来なさい
sen.comeOn();

代入演算子 `  がオブジェクトの名指しを意味します。

  • ` = ` の左側 がオブジェクトに付けられる変数名。

  • ` = ` の右側 が名指し対象のオブジェクト。

そして、代入演算子  ` `  はオブジェクトに対するメッセージの送信は行いません。

Java 言語での書き方まとめ

Java 言語でのコードとオブジェクトとの対応関係をまとめます。

  • メソッドは、オブジェクトに対するメッセージ送信を意味する。

  • 代入演算子 ` = `  は、変数によるオブジェクトの名指しを意味する。

    • オブジェクトに対するメッセージの送受信は意味しない。

    • オブジェクトに対して作用を与えない。

Java言語のメッセージ送信と名指し

Java 言語を利用したことのない方にとっても、理解しやすいと思います。

Python のインデックス構文と、それが払う小さな代償

Python のインデックス構文

一方、Python です。Python も OOP 言語なので、プログラムはすべてはオブジェクト間のメッセージのやり取りとして動きますし、Java 言語ふうに書けば、それらしく動きます。

ただし、Python には Java 言語ふうの書きかただけでなく、それをちょっとひとヒネりした書き方があります。このヒネり方には「プロパティ」と「インデックス」がありますが、ここでは インデックス のみ取り上げます(なぜなら、インデックスこそが SWCW の遠因になるからです)。

次の示すコードがインデックスをつかった書き方です。ではさて、このコードが行うのは、オブジェクトへのメッセージ送信でしょうか? それとも、オブジェクトの名指しでしょうか?

you['name'] = '千'

コードを文字通り素直に解釈すると、you['name']という変数で '千' オブジェクトを名指ししているように見えます。

ところが、Python インタプリタは、このコードで youオブジェクトにメッセージを送信します。 なぜなら、上記のコードは you.__setitem__('name', '千') の構文糖衣だからです。

Pythonのインデックス構文は 文字通りの解釈が行われない

なぜこんなややこしい事になっているのでしょう?

OOP が説く道

なぜなら、それこそが、オブジェクト指向プログラミング(OOP)だからです。ちゃんと書くと、「構造化されたデータはすべて、その構造ごとまとめて『オブジェクト』として扱われるべきである。そして、構造内の各要素データの読み書きは、すべてオブジェクトとのメッセージのやり取りを通して行わなけれればならない!」というのが OOP が説く道なのです。

OOPの説く道

この OOP の道を学ぶ価値はあるのでしょうか? 正直なところ、データ分析さえできればいい人にとっては、そんなものを学んでも、ほとんどメリットは無いかもしれません。

ですが、Python がなまじ OOP 言語であるため、pandas 利用者はこの考え方をうっすらで良いので、知っておく必要があるというわけです。

pd.DataFrame や np.ndarray も構造化されたデータですから、Pythonでその各要素データにアクセスするためには、かならずオブジェクトとメッセージのやり取りが行われているということになります。このメッセージのやり取りをパッと見で隠す構文こそが、インデックスやプロパティなのです。

さらにインデックス構文の例

つづけてもうすこし例を見てみましょう。

# 今からお前の名前は千だ。いいかい、千だよ。
you['name'] = '千'

# 名はなんという?
your_name = you['name']  # え?ち、…ぁ、千です。

自然に理解できるコードのようですが、文脈の違いにより、代入演算子 = に対して 二つの全く異なる解釈 が行われることが分かるでしょうか。

  • you['name'] = '千'
    オブジェクト you に対して、 __setitem__('name', '千')というメッセージを送信します。その作用で、オブジェクトyouの内容は変化します。

  • your_name = you['name']
    変数 your_nameに よる オブジェクト you['name']( = '千' )の名指しを行います。 代入演算子 = は オブジェクトに何の作用も与えません。

また、you['name'] という記述も、文脈の違いにより二つの異なる解釈が行われています。

  • you['name'] = '千'
    you.__setitem__('name', '千') の構文糖衣です。 オブジェクト you に対して、 __setitem__('name', '千') というメッセージが送信されます。

  • your_name = you['name']
    your_name = you.__getitem__('name') の構文糖衣です。 オブジェクト you に対して、 __getitem__('name') というメッセージが送信されます。

PythonのIndexing

ややこしい例

もっと、ややこしい例を紹介します。以下のコードはどう解釈でき、結果として何が起こるでしょうか?

you = {'name' : ['荻野','千尋']}
you['name'][1] = '千'
your_name = you['name'][1]
print(your_name) # '千'

works = ['ルパン', 'ナウシカ', 'ラピュタ', 'トトロ', '宅急便', '紅の豚']
works[:5][3] = '火垂る'
fourth = works[:5][3]
print(fourth)  # 'トトロ'

注意深く読めば、解釈は一意に定まりますが、自明とは言いづらくなってきましたね。

このややこしさが、Python の「インデックス」という構文糖衣がもたらす、小さな小さな代償です。そしてこのややこしさが、SWCW の難解さにつながるボタンの掛け違いの、始まりになるのだと思います。

インデックスの込み入った例

ところで、print(your_name) は '千' になるのに、 print(fourth) はなぜ '火垂る'ではなく 'トトロ' になるのでしょうか。分からない方は、次回のスライスの記事をお読みください。上記のコードの動作原理がわからないままでは、SWCW の理解まで辿り着けません。ヤッカイですね。

SWCW の原因は chained indexing ではない

ちょっと話を先取りするのですが、Python のインデックス構文は、SWCW を引き起こす遠因ではあっても、SWCW と直接の関係はありません。なぜなら、インデックスはあくまで構文糖衣でしかないからです。

にもかかわらず pandas 本家のサイトは、説明に chained indexing という言葉を使っています(you['name'][1] といったようにインデックスをつなげることを、chained indexing というようです)。この言葉による説明は、まったくミスリーディングだと私は思っています。

たとえば、次のコードは SettingWithCopyWarning を発生させます (pandas==2.2.2 で確認しました) が、このコードの果たしていったいどこに、 chained indexing があるというのでしょう。

import numpy as np
import pandas as pd
df = pd.DataFrame(np.random.rand(10, 3), columns=list('abc'))
df = df.query('a > 0.5').sort_values('c', inplace=True)

まとめと次回予告

今回は Python のインデックス構文が、オブジェクトへのメッセージ送信をコードの見た目上わかりづらくするさまについて確認しました。

次回は pandas の話、と行きたいところですが、そのまえに、リストのスライスと np.ndarray のスライスについて確認したいと思います。

#Python   #pandas   #OOP   #プログラミング #データ分析


いいなと思ったら応援しよう!