魔術師見習いのノート

プロフィール

魔術師見習い
Author魔術師見習い-_-.
Twitter魔術師見習い

コンピュータ関係のメモを主に書きます.

MENU

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では、ASCII文字以外の文字を使用できないが、Pythonでは、日本語名の変数や関数などが定義できる。なおCと同様に先頭文字に数字は使用できない。

また、Cでは次の名前はいくつかのライブラリや言語処理系で予約されている。

  • _が接頭辞のグローバルな名前
  • _が接頭辞でそれに大文字の名前
  • __が接頭辞の名前
これは言語的な制約ではなく、名前の競合を防ぐための注意やマナーである。

他にも、Cでは、ユーザが定数を全て大文字にしたり、グローバル変数の接頭辞をg_にしたりすることで、定義した場所以外でもそれらの意図を分かりやすくなるような工夫がある。これはコードの可読性を上げ、コードリーディングを助ける。Pythonには言語仕様で特別な意味を持つ名前がある。

  • _で始まる名前は、モジュールから*(ワイルドカード)でインポートする場合に除外される。
  • クラス内の__で始まるメンバの名前はクラス内でだけ有効である。
それゆえ前述の名前をその意味に反する意図で使用することはできない。なお2つ目のルールは、実際には呼び出せない訳ではなく、Pythonによって名前が変更され、オリジナルの名前でアクセスできない状態となる(それゆえdir関数で名前を調べられる)。


代入文

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
真はこれ以外である。ちなみにbool型はint型の部分型であり、Falseをint型に変換すると0となる。ただし整数型と異なり、文字列変換すると"True"や"False"になる。


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項演算子を次のように記述する。
a<10? a : 0
これに対し、Pythonでは同様の式を次のように記述する。
a if a<10 else 0
ブール演算
PythonとCのブール演算では演算子の字句が異なる。字句はそれぞれこのように対応する。
PythonC
not!
and&&
or||
また、ANDブール演算やORブール演算は、Cと同様に左辺から右辺に向けて順に評価される。そして、andは偽になった時点で、orは真になった時点で評価を終了する。ただし、CとPythonでは返り値が異なる。
言語真偽返り値
C1
0
Python最後に評価した値
真偽については「真偽」を参照されたし。
除算演算(//と/)
モジュロ演算

/が除算演算、//が切り捨て除算という。除算演算が小数まで計算するのに対し、切り捨て除算演算が剰余を切り捨てる。除数が0の場合、ZeroDivisionError例外が発生する。除算演算は、Python2と3で異なる処理を行う。Python2では、除算演算や切り捨て除算演算に関わらず、項が2つとも整数型または長整数型であれば、整数型や長整数型の除算結果を返す。しかしPython3では、除算演算の場合は浮動小数点型を返し、切り捨て除算の場合は2つの項が整数型か長整数型であれば、整数型を返す。

x % y
符号付き除算及びモジュロの演算結果は言語処理系に依存する。Pythonの場合、モジュロ演算の結果は、0か符号が常に除数(この場合y)と同じになる。モジュロ演算の結果の絶対値は、常に除数以下になる。また、Pythonでは、切り捨て除算演算結果と除数の積に剰余を加えた値は、元の値と常に等しい。
x == (x // y)*y + (x % y)
すなわち、このような被除数と除数の絶対値が5と2の除算やモジュロ演算の結果は次のようになる。
切り捨て除算演算除算結果モジュロ演算剰余
5//225%21
-5//-22-5%-2-1
-5//2-3-5%21
5//-2-35%-2-1
ちなみにJavaの場合、除算結果は0方向に切り捨てられ、かつ"x == (x / y)*y + (x % y)"はtrueとなるポリシーである。

なおPythonの%演算子は、標準で文字列オブジェクト用にオーバーロードされている。

シフト演算
シフト演算には、論理シフトや算術シフトなどの種類があり、Cのシフト演算がどれであるかは、曖昧である。これに対し、Pythonのシフト演算は算術シフトである。すなわち演算結果は次の式と同値となる。
x >> nx // pow(2,n)
x << nx * pow(2,n)
右辺の項は、負の値を指定するとValueError例外が、sys.maxsizeより大きいとOverflowError例外が発生する。
比較演算

Cと異なり比較演算子は全て同じ優先順位である。比較演算には次の種類がある。

  • <
  • <=
  • ==
  • !=
  • >=
  • >
  • is
  • is not
  • in
  • not in
Pythonでは、比較演算子はCとはやや異なる振る舞いをする。Pythonの比較では2つ以上の項を記述できる。これは可読性に優れているだけでなく、いくつかの場合に性能(効率)的にも優れている。

例えばCでこのような関数があったとする。

int func(){
  int retval = 1;
  retval += 1;
  return retval;
}
この時、func()の値の範囲を評価するコードを書くには、数学っぽくこのように書きたくなるかもしれない。
3 > func() > 1
しかし、Cでは>演算子は2項演算子であり、これは別の意味となる。この式では、はじめに3>func()を評価し、>演算子が1か0を、この例では1を返す。そしてその値(1)と1を比較し最終的にこの式文は0を返す。それゆえ、範囲を表すにはこのように記述する。
3 > func() && func() > 1
これと同じ意味のコードをPythonで書くとすれば、次のように記述できる。
3 > func() and func() > 1
しかしながらこのようなコードはfunc()が2回呼び出されるため冗長である。また、関数の結果が呼び出し毎に変わる場合(例えばretvalがstaticなローカル変数の場合)、ユーザの意図したコードと異なる場合がある。これを防ぐため、Cではこのような一時変数を使用することで回避してきた。
3 > (tmp=func()) && tmp > 1
これに対してPythonの比較演算子はこのような記述だけで良い。
3 > func() > 1
Pythonの比較演算子はANDブール演算のように各項を左から右へ評価していき、Falseの式より右辺を評価しない。とはいえ、機械語的に3項の比較など考え難いため、恐らくコンピュータ内部では2項の比較を何度も行っているはずである。


繰り返し文

Cでは繰り返し文を次のように記述する。

for文while文
for (初期化式;条件式;更新式)
  処理
while (条件式)
  処理
Cのfor文は次のような流れで繰り返され、いずれかの条件式の評価で偽の結果となった際に終了する。
  1. 初期化式
  2. 条件式
  3. 処理
  4. 更新式
  5. 条件式
  6. 処理
  7. 更新式
  8. 条件式
  9. ……

これに対してPythonのfor文は次のように記述する。

for ターゲットリスト in 式リスト :  
  処理1
[else:
 処理2]
while 条件式:
  処理1
[else:
  処理2]
ターゲットリストや式リストは次のように記述する。
>>> 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節というものが存在する。

一覧