LoginSignup
565
522

More than 3 years have passed since last update.

細かすぎて伝わりにくい、Pythonの本当の落とし穴10選+α

Last updated at Posted at 2019-06-15

Pythonはコードが書きやすい?ご冗談でしょう

・とにかくコロンを忘れまくる
・Pythonでは、関数が返す値には明示的に「return」を付ける必要がある
・リストに対してmapやfilterといった関数を適用した結果が、リストではなくイテレーターのオブジェクトになっている

こんなのを槍玉にあげてるの?ご冗談でしょう。

今こそあの記事を復活させなきゃいけない気がしたので、自分の昔のブログからサルベージ1しました。またせっかくなので、新しく加筆しました。Python の本当の落とし穴、ご査収ください。

自作の test.py を import しようとしてもできない

多くの初心者がハマることですが、自分で test.py というファイルを作って実行しようとしても、うまくいかないことがあります。

bash$ vi test.py      # test.py というファイルを作った
bash$ cat test.py
def func(x, y):
    return x, y
bash$ python
>>> import test       # これをimportすると、なぜか終了してしまう

----------------------------------------------------------------------
Ran 0 tests in 0.000s

OK
bash$ 

これは、Python には標準で test というモジュールが用意されており (!)、自作スクリプトよりそちらが優先されてしまうためです。

この標準モジュールは、自前で Python をコンパイル&インストールするときには必要だけど、それ以外ではほとんど必要とされません。しかし初心者を陥れる罠としては、とてもよく働いてくれるんです!

対策としては、test.pytest/__init__.py というファイルは作らず、tests.pytests/__ini__.py にするのがいいでしょう。

なお test.py に限らず、標準モジュールと同じ名前のスクリプトは作らないほうが身のためです。

スクリプトファイルを更新したのに、実行しても動作が変わらない

Python では、ソースファイル (*.py) を自動的にコンパイルして、バイトコードファイル (*.pyc、Java でいうところの *.class ファイル) を作ってくれます。これにより、2 回目以降におけるコードの読み込みが大幅に高速化されます。また、もとのソースファイル (*.py) が更新されると、必要に応じてバイトコードファイル (*.pyc) もコンパイルし直してくれます。便利ですね。

この自動コンパイルですが、Python では *.py のタイムスタンプが更新された場合に限り、*.pyc がコンパイルされます。逆にいえば、タイムスタンプが変わらなければ *.py を更新しても *.pyc がコンパイルし直されません。

例:

bash$ cat calc_add.py      # 足し算を行う関数を定義
def fn(x, y):
  return x+y
bash$ cat calc_mul.py      # 掛け算を行う関数を定義
def fn(x, y):
  return x*y

bash$ touch calc_add.py calc_mul.py   # 2つのタイムスタンプを揃える

bash$ cp -p calc_add.py calc.py   # 足し算バージョンを使う
bash$ python
>>> from ex1 import fn
>>> fn(3, 4)      # 足し算が行われることを確認
7
>>> ^D

bash$ cp -p calc_mul.py calc.py   # 掛け算バージョンに変更
bash$ python
>>> from ex1 import fn
>>> fn(3, 4)      # 掛け算ではなく足し算のまま!
7
>>> ^D

このように、Pythonでは *.py ファイルのタイムスタンプが更新されないと、*.pyc へのコンパイルが行われません。そのため、たとえばバックアップファイルから *.py を戻したのにプログラムの動作が変わってくれない、という落とし穴にハマることになります。

また Python は、*.py ファイルがなくても *.pyc さえあれば問題なく動作します。そのため、たとえば開発環境では *.py ファイルが消えていることに気づかず、本番環境にデプロイして初めて気づく、なんていうこともあります。

これらへの対策としては、*.pyc ファイルを適宜消すことです。通常はする必要はないですが、どうも変更が反映されないと思った時には、*.pyc をいったん消すのが安心です。

$ find . -type '*.pyc' | xargs rm     # bash
$ rm **/*.pyc                         # zsh

個人的には、タイムスタンプの問題はしょうがないにしても、*.py がなくても *.pyc だけで動いてしまうという仕様は、多くの場合においてトラブルの元になるのでやめてほしいです。たとえば、デフォルトでは *.py がないとエラーだけど、なにかオプションをつけて起動したときだけ *.py がなくてもエラーにしない、という仕様がよかったのではないかと思います。

exception Err1, Err2: と書くと Err2 を取りこぼす (Python2)

Python ではタプルを使うことで、1 つの except 節に複数の例外クラスを指定できます。


from datetime import date
try:
  date(2014, 2, 30)
except (TypeError, ValueError):  # 複数の例外クラスを一度に指定
  print("error")

しかしタプルを使うのを忘れると、Python2 では違う意味になってしまいます。

from datetime import date
try:
  date(2014, 2, 30)
except TypeError, ValueError:
  # ↑これは except TypeError as ValueError: と同じ
  print("error")

この場合であれば、TypeError は捕捉できますが ValueError はスルーしてしまいます。これはとても間違いやすいので、気をつけましょう。

なお Python3 ではこれは文法エラーになるため、間違えることはありません。

,」があるだけでタプルになってしまう

Python では、タプルのリテラルでは , があればよく、丸カッコは必ずしも必要ではありません。

## Pythonでのタプルの書き方
t1 = (1, 2, 3)    # 一般的にはこう書くことが多いが、
t2 = 1, 2, 3      # 実はこうでもよい

そして、要素が1つだけのタプルでも事情は同じです。

## 要素が1つだけのタプル
t3 = (100,)       # 一般的にはこう書くことが多いが、
t4 = 100,         # 実はこう書いてもよい

これを見ると分かるように、Python では末尾に , がついただけでタプルになってしまいます。これが初心者には気づきにくい落とし穴になることがあります。

たとえば、複数の長い文字列を渡す関数呼び出しがあったとしましょう。

func("The Python Tutorial",
     "This tutorial is part of Python's documentation set and ...",
     "https://docs.python.org/3.4/tutorial/")

これを、いったん説明変数に代入するようリファクタリングしたとします。

title = "The Python Tutorial"
desc  = "This tutorial is part of Python's documentation set and ...",
url   = "https://docs.python.org/3.4/tutorial/"
func(title, desc, url)

これでバグが入り込みました。desc に代入した文字列の末尾に , が残っているため、desc は文字列ではなくタプルになってしまっています!

こういう書き換えはよく行いますが、そのたびにこんなつまらないバグが入ることがあります。分かっていてもハマってしまう、とてもイラつく落とし穴です。

この落とし穴は、「タプルには丸カッコが必要」という言語仕様になっていれば防げます。なので、(異論はあるでしょうが)個人的には Python の設計ミスだと思ってます。

連続した文字列リテラルは自動的に連結されてしまう

Pythonでは、文字列リテラルが連続していると、自動的に連結されます。

## 3つの文字列リテラルがあっても、1つに連結される
s = "AA" "BB" "CC"
print(repr(s))   #=> 'AABBCC'

これを利用して、複数列の文字列を書くこともよく行われます。

## 複数行の文字列を書く例
if not valid_password(email, password):
    return (
        "ユーザ名またはパスワードが違います。\n"
        "(CapsLockがオンになってないことを確認してください。)\n"
        "パスワードがわからない場合はサポートまでご連絡ください。\n"
    )

しかし、これはちょっとしたことで思わぬバグを生み出す原因にもなります。たとえば次の例では、, を抜かしてしまったためにバグが入り込んでいます。しかも、ぱっと見ただけではどこが悪いのか気づきにくいです。どこにバグがあるか、わかりますか?

## バグのあるコード
month_names = [
    "January", "February", "March", "April", "May", "June", "July"
    "August", "September", "October," "November", "December"
]

Python の入門書によっては、「連続した文字列リテラルは連結される」という仕様を説明していないものもあるでしょう。そのような入門書で学んだ初心者が、このバグを理解できるかというと、ちょっと厳しいですよね。よって Python のこの仕様は、初心者にとって十分な落とし穴といえます。

(ところで上のコードには、バグが 2 つあります。1 つだけ見つけて「こんなの簡単じゃん!」と思った人はスクワット 30 回ね。)

そもそも、今の Python にはこの仕様は必要ないはずです。なぜなら、文字列リテラルを + 演算子でつなげば、コードの最適化により自動的に連結されるからです。これは次のようにバイトコードを調べればわかります。

>>> def fn():
...   return "X" + "Y" + "Z"  # ← 文字列リテラルを「+」でつなげると…
...
>>> import dis
>>> dis.dis(fn)
  2      0 LOAD_CONST      5 ('XYZ')   # ← バイトコード上では連結済
         3 RETURN_VALUE

このように、文字列リテラルどうしの連結は + 演算子の最適化に任せれば、「連続した文字列リテラルは連結される」という落とし穴はなくせるし、なくすべきです。Python2 は仕方ないにしても、Python3 でこの仕様が残っているのは失敗ではないでしょうか。

文字列の % 演算子が、タプルとそれ以外で挙動が変わる

Python では、str % arg という式があったとき、arg がタプルかそうでないかで挙動が変わります。

  • arg がタプル以外なら、% 演算子の引数は 1 つだけだと見なされます。
  • arg がタプルなら、% 演算子の引数は N 個 (N>=1) だと見なされます。

例:

"%r" % "a"          #=> 'a'
"%r" % [1, 2]       #=> '[1, 2]'
"%r" % (1, 2)       #=> TypeError: not all arguments converted during string formatting
"%r, %r" % (1, 2)   #=> '(1, 2)'

そのため、たとえば次のような関数にタプルを渡すと、意図しないエラーが発生します。

def validate(arg):
  if not isinstance(arg, str):
    errmsg = "%r: integer expected" % arg   # ここがバグ
    raise ValueError(errmsg)

validate(['3','4'])   # これは意図した通りのエラー
validate(('3','4'))   # これは意図しない TypeError が発生

この問題への対策としては、% 演算子を使わず format() メソッドを使うか、または str % arg を使わずすべて str % (arg,) にすることです。

def validate(arg):
  if not isinstance(arg, str):
    errmsg = "%r: integer expected" % (arg,)  # このほうが望ましい
    raise ValueError(errmsg)

bool型が、実はintのサブタイプである

Pythonでは、True==1False==0 が真となることが知られています。PHPの '0'==0 ほどひどくはないものの、あまりいい仕様ではないですね。

>>> True==1
True
>>> False==0
True

それだけではなく、なんと bool 型が int のサブタイプとなってます。

## なんでこんな仕様なんだろうか
>>> isinstance(True, int)
True
>>> isinstance(False, int)
True
>>> issubclass(bool, int)
True

この仕様のせいで、「int 型だと思った?実は TrueFalse でした!」というクソくだらないバグが入り込みます。

## 例1: 値が整数値であることを確かめてからinsert文を実行してるけど、
## 値がTrueやFalseだとバリデーションをすり抜けてしまい、SQLエラーになる
if not isinstance(value, int):
    raise TypeError("%r: integer expected." % (value,))
sql = "insert into tbl(intval) values (:intval)"
db.execute(sql, {'intval': value})

## 例2: JSONのプロパティ値が整数値であることを確かめてるけど、
## 実はTrueやFalseが返ってきてもテストがエラーにならない
def test_expected_int_value(self):
    response = requests.get('http://....')
    jdata = response.json()
    assert isinstance(jdata['value'], int)

実は昔の Python には TrueFalse がなく、かわりに 10 を使っていたらしいので、そのときの名残としてこんな仕様になったのでしょう。(実際、Python2 では TrueFalse は予約語ではなく、ただのグローバル変数です。知ってました?)

しかし Python2 ならまだわかるんですが、Python3 でもこの仕様を引きずっているのはどうなんでしょう? 言語仕様を整理するためのPython3なんでしょ? こういうとここそ直してほしかったです。

intとlongに共通する親クラスがない (Python2)

Python2 では、整数を表すのに int 型と long 型があります。int 型で表せないほどの大きな数になったら、自動的に long 型が使われます。

## MacOSXの場合
>>> 2**62
4611686018427387904       # ← 2の62乗は、int型
>>> 2**63
9223372036854775808L      # ← 2の63乗は、long型 (末尾の 'L' に注目)

とはいえ、「大きい数字でないと long 型にならない」というわけではありません。末尾に L をつければ、任意の整数を long 型にできます。

>>> type(1)
<type 'int'>
>>> type(1L)
<type 'long'>

このように、Python2 には整数を表すのに int 型と long 型があります。そのせいで、思わぬバグに遭遇することがあります。

たとえば、int 型だけを想定した関数に long 型を渡すと、エラーになることがあります。

## int型だけを想定した関数があったとして、
def build_json(intval):
    if not isinstance(intval, int):
        raise TypeError("%r: integer expected" % (intval,))
    return {"status": "OK", "value": intval}

## これにlong型を渡すと、TypeErrorになる
jdict = build_json(123L)           #=> TypeError
response.write(json.dumps(jdict))

「そんなん、わざと long 型を使ってるだけじゃん!普通に使ってる分にはひっかからないだろ!」という人もいるでしょうが、それは Python をあまり使ってないからでしょう。たとえばデータベースから取ってきた値は、小さい数でも long 型になっていることがあります。

count = db.execute("select count(*) from tbl").scalar()
print count         #=> 5
print type(count)   #=> <type 'long'>    # 整数値は、int型ではなくすべてlong型になっている

ここで、もし long 型が int 型のサブクラスであれば、上のような問題は起きません。しかし Python2 では、long 型は int 型のサブクラスではないし、両者に共通の親クラスも存在しません。str 型と unicode 型には basestring 型という共通の親クラスがあるというのに、そのような配慮が int 型と long 型にはないんです。

そのため、値が整数値かどうかを調べるには、Python2 では次のようにする必要があります。

if isinstance(value, (int, long)):   # isinstance(value, int) ではダメ
    print("OK")

bool 型も考慮すると、こうなります。

if isinstance(value, (int, long)) and not (isinstance(value, bool)):
    print("OK")

いくらなんでもこれは厳しい…

int 型と long 型があること自体は、そんなに悪いこととは思いません。しかし long 型と int 型に何の継承関係もないのは、どう考えても仕様のミスです。これについては異論は認めません。

なお Python3 では long 型はなくなり、大きい数もすべて int 型になります。すばらしい。

デコレータに複雑な式を書くと文法エラー

Python には、「デコレータには複雑な式を使って欲しくない」という設計意図があるらしいです。そのため、デコレータでメソッド呼び出しをチェーンさせると文法エラーになります。

例:

class cmdopt(object):
  _all = []
  def __init__(self, opt):
    self._opt = opt
    cmdopt._all.append(self)
  def arg(self, name, type=str):
    self._arg_name = name
    self._arg_type = type
    return self
  def __call__(self, func):
    self._callback = func

@cmdopt("-f").arg("file")    # たかがこの程度で文法エラー
def fn(arg):
  filename = arg
  with open(filename) as f:
    print(f.read())

実行結果:

$ python ex1.py
  File "ex1.py", line 13
    @cmdopt("-f").arg("file")
                 ^
SyntaxError: invalid syntax

このように、Python のデコレータには任意の式を書けるわけではなく、複雑な式が書けないよう意図的な制限が入っています。

はっきりいってこんな制約はいらないと思うのですが、これが Python way なんでしょう。おとなしく、メソッドチェーンのかわりにキーワード引数を使いましょう。

## キーワード引数が増えると複雑になるからメソッドチェーンを使ってるのに、
## メソッドチェーンはエラーにするくせにキーワード引数は制限なしというのが
## 納得できない
@cmdopt("-f", arg=("file", str), desc="data file")  # エラーにならない
def fn(arg):
  pass

同じコードでもPythonのバージョンによって挙動が変わる

滅多にあることではありませんが、Python でもバージョンによって、同じコードが動いたり動かなかったりします。

例を見てみましょう。まず、Python では、インスタンスオブジェクトごとに別々のメソッドを定義できます。

class Hello(object):
  def hello_english(self, name):
    return "Hello %s!" % (name,)
  def hello_french(self, name):
    return "Bonjor %s!" % (name,)

  def __init__(self, lang='en'):
    if lang == 'fr':
      self.hello = self.hello_french
    else:
      self.hello = self.hello_english

hello1 = Hello()
hello2 = Hello('fr')
print(hello1.hello("Python"))   #=> Hello Python!
print(hello2.hello("Python"))   #=> Bonjor Python!

これ自体は便利な機能ですが、たとえばこれが __enter__()__exit__() の場合、Python のバージョンによって動いたり動かなかったりします。

たとえば次のコードをご覧ください。

class Foo(object):

  def enter1(self):
    print("enter1")
  def enter2(self):
    print("enter2")
  def exit1(self, *args):
    print("exit1")
  def exit2(self, *args):
    print("exit2")

  def __init__(self, arg=1):
    if arg == 1:
      self.__enter__ = self.enter1
      self.__exit__  = self.exit1
    else:
      self.__enter__ = self.enter2
      self.__exit__  = self.exit2

obj = Foo()
with obj:
  print("with-statement")

これは、Python 2.6 や 3.0 や 3.1 では問題なく動作します。

bash$ py ex1.py
enter1
with-statement
exit1

しかし、Python 2.7 や Python 3.2 以降ではエラーになります。

bash$ py ex1.py
Traceback (most recent call last):
  File "ex1.py", line 22, in <module>
    with obj:
AttributeError: __exit__

このように、同じコードでも Python のバージョンによって動いたり動かなかったりします。しかもこれは、言語仕様をよくするためというより、単に Python の実装上の都合だったりします。

こういうのは Python では滅多にないんですが、まったくのゼロではないということは心に留めておいていいでしょう。

(New!) 引数のデフォルト値が共有される

Pythonでは引数のデフォルト値を指定できます。

## 引数のデフォルト値を指定しておくと…
def f1(n=0):
  print("n=%r" % (n,))

## 引数を省略すると、デフォルト値が使われる
f1()   #=> n=0

このとき []{} をデフォルト値に使うと、すべての関数呼び出しで同じリストや辞書が使われます。そのため、関数を呼び出すたびに空のリストや辞書が作られることを期待していたのに、実際には前回の呼び出し時と同じリストや辞書が使われてしまうというバグが発生します。

## デフォルト値として空のリストを指定する
def add(val, values=[]):  # 呼び出すたびに空リストが渡されることを期待
  values.append(val)      # 空リストに値を追加(してるつもり)
  return values           # リストを返す

## 呼び出すと、前の呼び出しの値もリストに含まれてしまう!
values = add(10); print(values)   #=> [10]
values = add(20); print(values)   #=> [10, 20]
values = add(30); print(values)   #=> [10, 20, 30]

なので、関数を呼び出すたびに空のリストや辞書が必要なら、デフォルト値として指定するのではなく自分でそれらを作る必要があります。

## デフォルト値としてはNoneを指定する
def add(val, values=None):
  if values is None:      # 引数が指定されてないときだけ
    values = []           #   空のリストを作成する
  values.append(val)      # 空リストに値を追加
  return values           # リストを返す

## 呼び出すと、期待したとおりの挙動になる
values = add(10); print(values)   #=> [10]
values = add(20); print(values)   #=> [20]
values = add(30); print(values)   #=> [30]

Rubyと比べると不便ですが、このくらいなら許容範囲でしょう。

ただ Python って、辞書のキーには不変な値(整数や文字列や真偽値やタプルやimmutable setなど)しか指定できず、可変な値(リストや辞書)は指定できないんですよね。そういう制約がつけられるなら、引数のデフォルト値も不変な値しか指定できないよう制約をつけてほしかったです。そうすれば引数のデフォルト値としてリストや辞書を指定するとエラーになるので、ここで説明したような落とし穴は防げたでしょう。

(New!) A is not BA is (not B) ではない

Python では、同値性を調べる == と同一性を調べる is の 2 つの演算子があります。

### 空のリストどうしを `==` で比較すると、
### 値として同じ(同値)なので True になる
>>> [] == []
True

### しかし `is` で比較すると、オブジェクトとしては
### 別なので(同一ではないので)、False になる
>>> [] is []
False

そして == の反対は != ですが、is の反対は is not です。is not は 2 つの単語で 1 つの意味になります。

### '==' の反対は '!='
>>> [] != []
False

### 'is' の反対は 'is not'
>>> [] is not []
True

この is not が誤解を招きやすいです。というのも、他の言語だと比較演算子より否定演算子のほうが優先順位が高いので、A == ! BA == (! B) という意味なんですよね。それと同じ感覚だと、A is not BA is (not B) だと思ってしまうでしょう。しかしすでに説明したように、is not は 2 つで 1 つの意味なので、A is (not B) ではなく、(文法エラーであることを無視すれば)A (is not) B という意味です。

このように、is not のように 2 つの単語で 1 つの意味になる演算子は誤解を招きやすいので、Python の理念には合わないと思います。かわりに isnot または isnt という専用のキーワード(予約語)を導入したほうが、紛らわしさがなくて Python らしかったはずです。Python はキーワードを増やすのを嫌がりますが、そのせいで 2 単語の演算子を使ったなら失敗だと思います。

(New!) ifが3つの意味を持っている

Python では新しいキーワードの導入をかなり嫌がります。その結果、同じキーワードに複数の意味を持たせることがあります。

たとえば Python では if に 3 つの意味があります。

  • if 文 (例:if condition: ...)
  • 条件演算子 (例:A if condition else B)
  • 内包表記でのフィルタ (例:[ ... for x in xs if condition])

どれも条件分岐に関連していますが、(似てはいるけど)どれも違う機能なので、これらを見分けるのは初心者には難しいです。

これら 3 つとも使った例を見てみましょう。

def example(xs):
  if xs:                             # if文
    return [ x if x >= 0 else - x    # 条件演算子
                 for x in xs if x ]  # フィルタ
  return []

初心者がこれを見て 3 つある if の使い分けを理解できると思うなら、あなたは初心者を教えるべきではないです。初心者にとってはいい迷惑でしょう。初心者は式や文の違いだって分からないのだから、このサンプルコードの理解は困難です。

このように、Python では新しいキーワードの導入を渋ったせいで 1 つのキーワードに複数の意味を持たせてしまい、結果として初心者には理解しにくいコードになることがあります。Python 上級者がこのことを認めることはないでしょうが、1 つのキーワードに複数の機能を持たせるのは、初心者にとっての分かりやすさという観点からは失敗でしょう。

(New!) 通常の関数とジェネレータ関数がどちらもdefで定義

先に述べたように、Python では新しいキーワードの導入を嫌がります。そのせいで、ジェネレータ関数の定義は通常の関数の定義と同じように def を使います。

ジェネレータ関数の例:

def f(n):
  yield n
  yield n+1
  yield n+2

## 使い方
for x in f(10):
  print(x)

実行例

10
11
12

このように、ジェネレータ関数は def で定義します。これは通常の関数と同じですよね。違いは関数定義の中に yield があるかどうかです。

  • 関数定義の中に yield があればジェネレータ関数
  • そうでなければ通常の関数

それでは次の関数は、ジェネレータ関数でしょうか、通常の関数でしょうか。

def f():
  if False:     # if False なので、
    yield       # この yield が実行されることはない
  return 1

答えは、「ジェネレータ関数」です。なぜなら関数定義の中に yield が入っているからです。たとえ if False のせいで実行されなくても関係ありません。

では実行されない if False: yield を取り除くとどうなるでしょうか。

def f():
  #if False:    # 実行されないのでコメントアウト
  #  yield
  return 1

こうすると、通常の関数になります。なぜなら関数定義中に yield がないからです。

このように、def を使った同じ構文なのに yield のある・なしで意味が変わります。これはあまりよくないですよね。特に実行されないはずの yield が意味を持つ(実行されない文を取り除くと意味が変わる)のは直感的とは言えません。

実はちょうどこの前、これで失敗したところでした。あるデバッグを追っているときに、時間のかかる箇所をコメントアウトしたんですね。そしたらそれが yield を含んだ箇所だったので、ジェネレータ関数が通常の関数になってしまい、結果として違うところで別のバグが出てしまって混乱しました。やはり通常の関数とジェネレータ関数とが同じ構文なのは落とし穴だと思います。

ところでJavaScriptでは、通常の関数は function、ジェネレータ関数は function* を使って定義します。こちらのほうが分かりやすいし、新しいキーワードを導入する必要もありません。この仕様を考えた人はよい仕事をしました。Python もそうして欲しかったです。

/// JavaScript: 通常の関数
function f1() {
  return [1, 2, 3];
}

/// JavaScript: ジェネレータ関数(functionのあとに「*」がついている)
function* f2() {
  yield 1;
  yield 2;
  yield 3;
}

(New!) オブジェクトがNoneじゃないのにif文で偽となる

Python では、FalseNone だけでなく 0 や空文字列や空リストも偽とみなされます。

if False: print("False")    # 表示されない
if None:  print("None")     # 表示されない
if 0:     print("0")        # 表示されない
if '':    print("''")       # 表示されない
if []:    print("[]")       # 表示されない

これはつまり、値の種類によってどんな値が偽となるかが違うため、それらをすべて把握しておかないと Python の if 文は理解できないということです。

「そのくらい簡単でしょ!?」と思ったあなたはきっと Python の経験が浅いのでしょう。外部ライブラリを使っていると、思わぬオブジェクトが偽と扱われてしまうことがあります。

import sys

## 例1:ロガーがあるかどうかを「if logger」で判断
import logging
logger = logging.getLogger(__name__)  # ロガーオブジェクトを作成
if logger:                            # ロガーがあれば
  logger.error("logger exists")       # それを使う  ← 期待する動作
else:                                 # ロガーがなければ
  sys.stderr.write("no logger found") # 標準エラー出力を使う ← 期待してない

## 例2:フォームオブジェクトがあるかどうかを「if form」で判断
import cgi
form = cgi.FieldStorage()  # フォームオブジェクトを作成
assert form is not None    # Noneではないことを確認
if form:                   # 真値なら
  print(repr(form))        # 中身を表示  ← 期待する動作
else:                      # 偽値なら
  print("form is None")    # 別のメッセージを表示 ← 期待してない

実行結果

logger exists     ← 例1の期待した動作
form is None      ← 例2の期待してない動作

このサンプルコードはちょっと長いですが、要は

  • 例1の if logger: では、loggerNone ではないので真として扱われたのに対し、
  • 例2の if form: では、formNone ではないのに偽として扱われました。

いやーこれは分かりにくいですね。これを分かりにくいと思わない人がいたら、Python の欠点を絶対に認めないマンでしょう。

例2において if form: が偽と見なされたのは、フォームオブジェクトがコンテナ、つまりリストや辞書の一種だと見なされたからです。「見なされた」というのは正確ではないですね、フォームオブジェクトがコンテナとして振る舞うよう設計されたからです(詳しく知りたい人は「Python __bool__」や「Python __len__」で検索するといいでしょう)。

フォームオブジェクトは辞書オブジェクトのように振る舞うので、空の辞書が偽と見なされるように、空のフォームも偽と見なされます。言われてみれば当たり前ですが、言われないと気づきませんよね。

このような落とし穴を避けるために、if form: ではなく if form is not None: と書くのが望ましいです。ちょっと面倒ですけど、そう書く価値はあります。

おまけ:元記事コメントへの返信

元のブログ記事にコメントがついてましたが、返信してなかったので、この場で返信しておきます。遅レスですまん。

by xik:

(ところで上のコードには、バグが 2 つあります。1 つだけ見つけて「こんなの簡単じゃん!」と思った人はスクワット 30 回ね。)

あと、スペルミスが1つありますね( ̄ー ̄)ニヤ

このたび修正しました。すまんかった。

by Johann:

pycって本当に邪魔。最近の実行環境だとパフォーマンスゲインも無いみたいだし、いっそのこと一切pycを吐かない設定があればいいのにね。

いやいや、pycは有用だから。問題点があるからといって全てを否定するのはおかしい。

by Tossi:

pycって本当に邪魔。

http://docs.python.jp/2/using/cmdline.html#envvar-PYTHONDONTWRITEBYTECODE

この環境変数が設定されていると、 Python はソースモジュールの import 時に .pyc, .pyo ファイルを生成しません。

バージョン 2.6 で追加.

補足サンクス。

by sada:

コマンドラインと環境 ― Python 2.7ja1 documentation

この環境変数が設定されていると、 Python はソースモジュールの import 時に .pyc, .pyo ファイルを生成しません。

こちらもサンクス。ただ問題にしてるのは *.pyc の機能じゃなくて、*.py がなくても *.pyc だけで動作する点。*.pyc を吐かないことは解決策じゃない。

by Tossi:

intとlongに共通する親クラスがない (Python2)

numbers ― 数の抽象基底クラスが使えます

 Python 2.7.8 (default, Jun 30 2014, 16:08:48) [MSC v.1500 64 bit (AMD64)] on win32
 Type “help”, “copyright”, “credits” or “license” for more information.
 >>> import numbers
 >>> issubclass(int, numbers.Integral)
 True
 >>> issubclass(long, numbers.Integral)
 True
 >>> issubclass(bool, numbers.Integral)
 True

問題はそこじゃなくて、isinstance(1, long) is Falseisinstance(1L, int) is False であること。

by Mattun:

(ところで上のコードには、バグが 2 つあります。
私の誕生月も書いていないとは何事だ(笑)

このたび修正しました。すまんかった。

by 通りすがり:

ちょっとあまりにも記事内容がヒドいですね…。
この記事に書かれている殆どの参考コードは、自分自身のコーディングミスから起因しているものが大半です。
それをバグとかゴミとか仕様が意味不明だとかよく言えたものですね…。

本当のプログラミングは、”””よく知りもしないで勝手な自分の思い込みで作業はすべきではありません。”””
どの言語でもあるように、””””正しいコーディング規約に従って開発するべきです。”””

Pythonは拡張性が高く非常に優れた言語です。
みなさん、どうかこんなバカな記事を鵜呑みにしないで下さいね。

コーディングミスを引き起こす Python の落とし穴を説明してるんだけどね。Python 信者乙。

by 一応プロ:

高級言語の重要な目的の一つはバグを減らす、デバックを容易にするということです。
そのためのわかりやすさ、可読性、曖昧さの排除であり、そのために冗長な表現となっています。自分が完全に機械に合わせられると豪語するなら、マシン語で最初からバグのないプログラムを書けばOK。高級言語に口出ししなくていいです。
また、ITにおいての共通認識は、「人間はミスをするもの」です。ミスしないことを前提とした言語は、プロとしては使い物にならない「玩具」です。
また、プログラムは多人数で組みますので、ひとりだけのヒーローとか要らないです。

これはまた文章の読めない人だ。「自称」プロ?

by treasurebox:

私には大変役立ちました。丁寧な説明がうれしい。将来はまらなくて済みます。

あざす。次は君が書く番だ。

by 文字コード:

あれ?

Python2でつまずくといえば、真っ先に文字コードの話が出てくると思うんですが。

unicode文字列とbyte文字列
str型とunicode型とutf-8
encodeメソッドとdecodeメソッドとunicode関数
文字列リテラルの前につけるuとかrとか。

遭遇しがちなエラーメッセージもふまえて、上記各種がまとめてあると完璧ですよね。

文字コードがつまづきやすいのはその通り。ただそれは初心者にも広まっており、あまり知られてない落とし穴を紹介したいので省いた。指摘さんきゅー。

おまけ2:Twitterでの反応

すでに書いたとおりだけど、*.py がなくても *.pyc だけで動作することが問題。*.pyc を吐かないことは解決策じゃない。せいぜいworkaround。
しかしイヤミっぽく言う人だな。

"A" "B""AB" に結合される仕様も、bool が実は int なのも、落とし穴になるしデメリットが大きいことは説明したとおり。C言語にある仕様がすべて優れているわけないし「C言語にある仕様だから」というのは正当化する理由にならない(このくらい分かるよね?)。
また言語のspecに書いてあっても容易に落とし穴となる仕様なら批判されてもおかしくない。
おちょくった言い方する前にもうちょっとロジカルに考えられるようになりましょう。

おわりに

あの記事で挙げられている中で Python の問題点といえるものは何ひとつありません。自分が知ってる Scheme や Ruby と違うというだけで問題視するのは、Python だけでなく Scheme や Ruby にとってもいい迷惑です。

しかしプログラミングを勉強してくれてるマスコミの人なんてほとんどいないので、これからも勉強を続けてください。自分の専門以外のことを勉強するの、とても大事。ITエンジニアも在庫管理や会計を勉強してほしい。

565
522
19

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
565
522