Python

Pythonを始めた時に戸惑ったこと

More than 1 year has passed since last update.

初めに

今まで、いろいろなプログラム言語をつまみ食いしてきた。
古くは、C/C++, perl, sh, VBA, java あたり。
ここ数年では、C#, PHP, R, JavaScript。
最近になって、Python。

プログラム言語は互いに影響を受けあっているので、概念が同じで構文が違うだけということが多い。
ただ、同じ概念でも言語によって制約があったりして、そういった部分で戸惑うことがある。

ということで、Python を始めた時に戸惑ったことを、つらつらと書いてみる。

無名関数

これが一番戸惑った。

JavaScript では、function 式(アロー式でもよい)で関数オブジェクトを作れる。
関数オブジェクトは普通の関数なので、好きなように処理を書ける。
関数オブジェクトに名前を付ける必要がないから、無名関数として使える。

function.js
inc = function(x) { return x + 1; }
// inc(2) = 3
twice = (x) => x * 2;
// twice(4) = 8
fact = function(n) {
  var ret = 1;
  for (var i = 1; i <= n; ++i ) { ret *= i; }
  return ret;
};
// fact(4) = 24

C# のラムダ式も、関数本体の処理は自由に書ける。

funciton.cs
delegate int del(int a);

del inc = x => x + 1;
// inc(2) = 3
del twice = x => x * 2;
// twice(4) = 8
del fact = x => {
  var ret = 1;
  for (var i = 1; i <= x; ++i) { ret *= i; }
  return ret;
}
// fact(4) = 24

Python では、関数オブジェクトを作るのに def 文と lambda 式の2通りがある。
def 文では、好きなように処理を書けるが、定義時に名前が必要になる。
一方の lambda 式では、関数本体は式のみで文が使えない。if 文も for 文も代入文すらも。
簡単な処理を渡したい時に、トリッキーな書き方をするか、def 文で名前付き関数を定義する必要があり、不便。

function.py
inc = lambda x: x + 1
# inc(2) = 3
twice = lambda x: x * 2
# twice(4) = 8
fact = x => {
  ret = 1;
  for i in range(1, x+1):
    ret *= i
  return ret;
}
# fact is SyntaxError
fact = x => (x == 0) or (x * fact(x - 1))
# fact(4) = 24

ブロック区切りは{}ではなくインデント

C/C++ も C# も JavaScript も、ブロック区切りは{}
beginend を使う言語もあるが、とにかく、なんらかのトークンでブロックを区切る言語がほとんど。
インデントがブロック区切りになる Python はかなり珍しい部類。
でも、エディタの補助もあるし、少し Python のコードを書けばすぐに慣れるので問題無い。

と思っていたら、式の途中での改行で少しはまった。

今まで、長い式は二項演算子(+, -, and, or など)の前で改行して、行頭に演算子が来るようにしていた。
この方法なら、行頭を縦に読めば項と項の関係がすぐにわかる。
それぞれの項を詳細に理解するのは後でよい。

indent.js
if (very_very_very_long_condition
 && very_long_condition
 || very_very_very_very_long_condition) {
  // processing ...
}

v = compute_a_value_from_a_lot_of_objects
  + property_value_of_an_object
  - compute_a_value_from_two_objects;

Python は、行末が式の終わりになってしまうのでそうはいかない。
行末に \ を置くか、式全体を括弧で括るか、どちらにせよエレガントとは言い難い。
PEP8 では演算子を行末に置くことを推奨しているが、行末は視線を大きく滑らせないといけないし凸凹しているので読みづらい。
if 文なんか、: までの改行を無視してくれてもいいのに。

indent.py
if very_very_very_long_condition_1 and
    very_long_condition_2 or
    very_very_very_very_long_condition_3:
  // processing ...
}
if very_very_very_long_condition_1 \
    and very_long_condition_2 \
    or very_very_very_very_long_condition_3:
  // processing ...
}

v = compute_a_value_from_a_lot_of_objects +
    property_value_of_an_object -
    compute_a_value_from_two_objects
v = compute_a_value_from_a_lot_of_objects \
  + property_value_of_an_object \
  - compute_a_value_from_two_objects

オブジェクトと辞書

JavaScript も Python も、緩い型付けのオブジェクト指向である点は同じ。
オブジェクトにメンバーを後から追加できるのも同じ。
でも、オブジェクトの扱いがちょっと違う。

JavaScript では、オブジェクトごとにメンバを追加・変更・削除できる。
さらに、オブジェクトと辞書は同一で、メンバにはドット演算子( . )でも添え字演算子( [] )でもアクセスできる。
これは、他の言語にはあまり見られない JavaScript の特異な点である。
おかげで、参照するメンバを実行時に決めるといった、かなり変なこともできる。

dict.js
obj.foo = 10;
obj["bar"] = 20;
obj.func0 = function() { return 30; }
obj.func1 = function() { return 40; }

a = obj["foo"] // a = 10
b = obj.bar // b = 20
for (var i = 0; i < 2; ++i) {
  console.log(a["func"+i]());
} // 30, 40 

Pythonでは、他の多くの言語同様、オブジェクトと辞書は区別されている。
オブジェクトのメンバを参照するには、ドット演算子を使う。
文字列でメンバを参照できなくもないけれど、ちょっと面倒。
辞書はdict型のオブジェクトで、キーを使って値にアクセスするには添え字演算子を使う。

dict.py
o1 = type('', (), {}) # 空クラスのオブジェクト
o1.mem1 = 20 # メンバを参照
o1['mem1'] # NG

d = {'key1': 10, 'key2': 'foo'} # 辞書
d['key1'] # = 10
d.key2 # NG
d['key3'] = o1 # キーを追加

オブジェクトとメンバ

C#, C++, Java などは、強い型付けのオブジェクト指向。
オブジェクトから参照できるメンバは、自身のクラス階層できっちり決まる。
オブジェクトに後からメンバを追加・変更・削除はできない。
(コールバックとかデリゲートとかは、メンバの追加とは別物。)

JavaScript も Python も、緩い型付けのオブジェクト指向である点は同じ。
オブジェクトにメンバを後から追加・変更・削除できるのも同じ。
だが、その扱いがちょっと違う。

JavaScript では、ほぼ全てのものがオブジェクトで、メンバを編集できる。
そのメンバも、プロパティ・メソッドの別はなく、オブジェクトのプロパティでもメソッドでも編集できる。

member.js
function C()
{
  this.p1 = 10;
}
C.prototype.f1 = function() { return this.p1 * 5; }

c = new C();
c.p1; // 10
c.f1(); // 50
c.f2 = function() { return c.p2 * c.p1 };
c.p2 = 20;
c.p2; // 20
c.f2(); // 20 * 50 = 1000

Python でも、オブジェクトのプロパティは追加・削除できる。
しかし、メソッドの追加・変更・削除はできない。
メソッドのつもりで追加しても、プロパティに関数オブジェクトを突っ込んだだけなので、self が期待通りに扱われない。
また、クラス次第では、プロパティの追加・削除もできなかったりする。

member.py
class MyClass:
  def __init__(self):
    self.p1 = 10
  def f1(self):
    return self.p1 + 2

mc1 = MyClass() # 自作クラスのオブジェクト
mc1.p1 # 10
mc1.f1 # 12
mc1.p2 = 20 # プロパティを追加
del mc1.p1 # プロパティ p1 を削除
del mc1.f1 # NG: プロパティは削除できない
mc2 = MyClass()
mc2.p1 # 10 # 他の MyClass インスタンスにはメンバ p1 がある
mc2.f2 = lambda self: self.p1 + 5 # メソッドの追加に見える
mc2.f2() # NG。missing 1 argument 'self' # f2 は単なる関数オブジェクト
mc2.f2(mc2) # 15

o2 = object() # object クラスのオブジェクト
o2.p1 = 30 # メンバの追加は NG