CプログラマのPythonの落とし穴
投稿日: | |
---|---|
タグ: |
本稿は私のPython学習のメモやまとめ、特にPythonを学ぶ上でCプログラマから見た視点で気をつけなければいけないことや特徴をまとめたものである。学習には主に次の2つの情報源を使用した。
文法
Cでは、空白文字(スペースや改行、タブ)に字句の区切り以外の意味はない。そして、文は;(セミコロン)までを1文として、ブロックは{と}を使って表す。それゆえ"Hello, World"を画面に出力するためのプログラムは次のように記述できる。
int printf(const char *, ...);int func(){int i=0;while(i<3){printf("Hello, World\n");i += 1;}return 0;}
しかし多くのユーザは、次のような工夫によってコードの可読性を挙げる。
- 1文を1行に記述して文の区切りを分かりやすくする。
- 同じブロックの文を同じサイズのインデントにしてブロック階層を分かりやすくする。
int printf(const char *, ...);
int func()
{
int i=0;
while(i<3){
puts("Hello, World");
i += 1;
}
return 0;
}
Pythonでは、言語仕様によって改行やインデントで文やブロックの区切りを表すように定められている。それゆえ、自然と可読性の高いコードとなる。以下に前述のコードをPythonで記述したものを紹介する。
def func():
i = 0
while(i<3):
print("Hello, World")
i += 1
return 0
文の区切りはともかく、ブロックの終端字句がない仕様は、慣れないと違和感があるかもしれないが、デタラメなインデントのコードを読むよりは遥かに読み易いだろう。
命名規則
Pythonの名前(識別子)に使用できる文字は、次のいずれかである。
- 大文字アルファベット
- 小文字アルファベット
- アンダースコア(_)
- 数字
- ASCII以外の文字(詳しくはPEP-3131を参照されたし)
また、Cでは次の名前はいくつかのライブラリや言語処理系で予約されている。
- _が接頭辞のグローバルな名前
- _が接頭辞でそれに大文字の名前
- __が接頭辞の名前
他にも、Cでは、ユーザが定数を全て大文字にしたり、グローバル変数の接頭辞をg_にしたりすることで、定義した場所以外でもそれらの意図を分かりやすくなるような工夫がある。これはコードの可読性を上げ、コードリーディングを助ける。Pythonには言語仕様で特別な意味を持つ名前がある。
- _で始まる名前は、モジュールから*(ワイルドカード)でインポートする場合に除外される。
- クラス内の__で始まるメンバの名前はクラス内でだけ有効である。
代入文
Pythonでは代入は演算ではなく文である。Cの代入は演算であり、かつ代入した値を返すため、このように複合式で一時変数に代入するような記述ができた(後述の通りそもそもPythonではこのような一時変数は不要だが)。
if (1 < (tmp = func()) && tmp < 5) ) printf("hoge");
しかしPythonでは代入は式文ではなく1つの文であり、このような記述はできない。とはいえ、代入文の=の右辺に式を書くことは可能であるため、次のような記述は問題ない。
a = 1 + 2
なおCにあった+=や-=のような複合代入演算もPythonでは1つの文である。このような文は累算代入文(augmented assignement statement)という。
真偽
Cでは、0や0に変換される値(0.0や'\0'、NULLなど)が偽で、それ以外の全ての値は真である。これに対して、Pythonで偽とみなされる値は次のいずれかである。
- False
- None
- 空文字列
- 空のコンテナ
- 全ての型の値の0
None
Cのvoidに該当する型として、Pythonには値が存在しないことを表すNone型がある。Cのvoid型がオブジェクトも値も持っていないのに対し、None型のオブジェクトは1つだけあり、値も1つ(None)だけある。それゆえ、引数としてNoneを指定でき、返り値もなにも返さない訳ではなくNoneを返す。Pythonの関数がreturn文に到達しなかった場合、その関数はNoneの値を返す。
def func():
pass
print(func())
Cでは、返り値を持たない関数が存在する。それらはサブルーチンのような意図で記述されるものだが、定義上それは関数である。しかし、Pythonには値がないというコンセプトのデータを返すことで、全ての関数が返り値を持つ。
Noneの値はCのNULLポインタに該当し、評価結果は常に偽となる。
演算
- 条件演算(3項演算)
- 除算演算とモジュロ演算
- シフト演算
- 比較演算
- ブール演算
- 条件演算(3項演算子)
-
Pythonも条件演算をサポートするが、Cとは書き方が異なる。Cでは3項演算子を次のように記述する。
これに対し、Pythonでは同様の式を次のように記述する。a<10? a : 0
a if a<10 else 0
- ブール演算
-
PythonとCのブール演算では演算子の字句が異なる。字句はそれぞれこのように対応する。
Python C not ! and && or || 言語 真偽 返り値 C 真 1 偽 0 Python 真 最後に評価した値 偽 - 除算演算(//と/)
- モジュロ演算
/が除算演算、//が切り捨て除算という。除算演算が小数まで計算するのに対し、切り捨て除算演算が剰余を切り捨てる。除数が0の場合、ZeroDivisionError例外が発生する。除算演算は、Python2と3で異なる処理を行う。Python2では、除算演算や切り捨て除算演算に関わらず、項が2つとも整数型または長整数型であれば、整数型や長整数型の除算結果を返す。しかしPython3では、除算演算の場合は浮動小数点型を返し、切り捨て除算の場合は2つの項が整数型か長整数型であれば、整数型を返す。
符号付き除算及びモジュロの演算結果は言語処理系に依存する。Pythonの場合、モジュロ演算の結果は、0か符号が常に除数(この場合y)と同じになる。モジュロ演算の結果の絶対値は、常に除数以下になる。また、Pythonでは、切り捨て除算演算結果と除数の積に剰余を加えた値は、元の値と常に等しい。x % y
すなわち、このような被除数と除数の絶対値が5と2の除算やモジュロ演算の結果は次のようになる。x == (x // y)*y + (x % y)
切り捨て除算演算 除算結果 モジュロ演算 剰余 5//2 2 5%2 1 -5//-2 2 -5%-2 -1 -5//2 -3 -5%2 1 5//-2 -3 5%-2 -1 なおPythonの%演算子は、標準で文字列オブジェクト用にオーバーロードされている。
- シフト演算
-
シフト演算には、論理シフトや算術シフトなどの種類があり、Cのシフト演算がどれであるかは、曖昧である。これに対し、Pythonのシフト演算は算術シフトである。すなわち演算結果は次の式と同値となる。
x >> n x // pow(2,n) x << n x * pow(2,n) - 比較演算
Cと異なり比較演算子は全て同じ優先順位である。比較演算には次の種類がある。
- <
- <=
- ==
- !=
- >=
- >
- is
- is not
- in
- not in
例えばCでこのような関数があったとする。
この時、func()の値の範囲を評価するコードを書くには、数学っぽくこのように書きたくなるかもしれない。int func(){ int retval = 1; retval += 1; return retval; }
しかし、Cでは>演算子は2項演算子であり、これは別の意味となる。この式では、はじめに3>func()を評価し、>演算子が1か0を、この例では1を返す。そしてその値(1)と1を比較し最終的にこの式文は0を返す。それゆえ、範囲を表すにはこのように記述する。3 > func() > 1
これと同じ意味のコードをPythonで書くとすれば、次のように記述できる。3 > func() && func() > 1
しかしながらこのようなコードはfunc()が2回呼び出されるため冗長である。また、関数の結果が呼び出し毎に変わる場合(例えばretvalがstaticなローカル変数の場合)、ユーザの意図したコードと異なる場合がある。これを防ぐため、Cではこのような一時変数を使用することで回避してきた。3 > func() and func() > 1
これに対してPythonの比較演算子はこのような記述だけで良い。3 > (tmp=func()) && tmp > 1
Pythonの比較演算子はANDブール演算のように各項を左から右へ評価していき、Falseの式より右辺を評価しない。とはいえ、機械語的に3項の比較など考え難いため、恐らくコンピュータ内部では2項の比較を何度も行っているはずである。3 > func() > 1
繰り返し文
Cでは繰り返し文を次のように記述する。
for文 | while文 |
---|---|
|
|
- 初期化式
- 条件式
- 処理
- 更新式
- 条件式
- 処理
- 更新式
- 条件式
- ……
これに対してPythonのfor文は次のように記述する。
|
|
>>> for x, y in [0,10], [5,20], [10,30]:
... print(str(x) + "," + str(y))
...
0,10
5,20
10,30
式リストは1度だけ評価される。例えば式リストに組み込み関数rangeを使用し、"range(1, 5)"と記述した場合、"[1, 2, 3, 4]"となり、各要素が順に代入される。
Pythonのfor文やwhile文にはelse節がある。もしループの条件式の評価結果が偽となった場合、ループが終了した後に実行される。break文で終了した場合、else節はスキップされる。
型
整数
Cの整数型の詳細(サイズや表現可能な範囲など)は、言語処理系に依存する傾向が強く、かつ符号の有無や精度などの組合せでさまざまな種類が存在する。これに対して、Pythonの整数型はシンプルで、メモリサイズの制限があるだけで無限の定義域を持つ。また、負の数は2の補数で表しており、符号ビットが左に無限に続くような値となる。
Pythonの整数リテラルは、次の通りである。
表記 | 接頭辞 |
---|---|
16進数 | 0x, 0X |
10進数 | |
8進数 | 0 |
2進数 | 0b, 0B |
Cでは10と8,16進数をサポートしていたが、Pythonでは更に2進数もサポートする。
文字列
Cには、文字列型という組み込み型は存在せず、文字型配列を用いて文字列を表現する。これに対してPythonでは、str型という組み込み型を使用する。str型に関する詳細は、help関数を用いれば知ることができる。
Pythonでは、全ての型の基となるobject型に__str__や__repr__という関数が定義されており、それを上書きすることでstr型への変換の振る舞いを設定できる。
Cの文字列リテラルは、引用符に二重引用符を用いて表す。これに対し、Pythonではさまざまな表現方法がある。Pythonで使用される文字列リテラルの引用符は次の通りである。
文字列 | 引用符 | 説明 |
---|---|---|
'string' | 一重引用符 | |
"string" | 二重引用符 | |
'''string''' | 三連一重引用符 | エスケープなしで改行や引用符を書くことが可能。 |
"""string""" | 三連二重引用符 |
データ構造
複数のデータをまとめて扱う方法として、Cでは派生型の配列型やポインタ型を使用する。これに対し、C以降に登場した多くの言語では、そのためのさまざまな型がある。Pythonの場合、以下の3系統のデータ構造がある。
名前 | 型 | 説明 | リテラル |
---|---|---|---|
シーケンス型 | list | ミュータブルな可変サイズの配列 | [0, 1, 2] |
tuple | イミュータブルな可変サイズの配列 | (0, 1, 2) | |
str | 文字列 | "abcd" | |
集合型 | set | 集合 | {0, 1, 2} |
frozenset | |||
辞書型 | dict | ハッシュ | {"apple":1, "banana":3} |
これらのデータ構造にはさまざまな演算子や関数が用意されている。
その他
- 変数
- Cの変数が特定の型を持ちそのオブジェクトだけを格納だけたのに対し、Pythonの変数はどんな変数でも代入できる。
- 定数
- Cではマクロを使って定数のように扱う記述やconst指定子による定数がある。これに対し、Pythonには定数がない。ただし、一部のユーザは、定数の意図で使用する変数を命名規則で全て大文字にしている。
- コメント
- Cではコード中に記述するユーザのための文字列はコメントだけだったが、Pythonにはドキュメントを表すドキュメンテーション文字列とコメントがある。ドキュメンテーション文字列はユーザのために記述するが、コード的に意味を持つ。
- Cでは/*から*/までのブロックコメントと、//から行末までの2種類のコメントがある。これに対し、Pythonでは#から行末までのコメントしかない。
- if文
- Cではif節とelse節しかなく、複数の条件を書く場合はelse節の中にif節を書いていたが、Pythonではelif節というものが存在する。