開発中によく遭遇するPythonトラップと注意点

24557 ワード

可変オブジェクトをデフォルトパラメータとして使用しない
関数を使用するときは、デフォルトのパラメータが頻繁に使用されます.Pythonでは、デフォルトのパラメータとして可変オブジェクトを使用すると、予期せぬ結果が発生する可能性があります.
次に例を示します.
def append_item(a = 1, b = []):
    b.append(a)
    print b
    
append_item(a=1)
append_item(a=3)
append_item(a=5)  

結果:
[1]
[1, 3]
[1, 3, 5]

結果から、後の2回にわたってappend_item関数が呼び出されると、関数パラメータbは[]に初期化されず、前の関数呼び出しの値が保持されることがわかる.
この結果が得られたのは,Pythonにおいて1つの関数パラメータのデフォルト値が,その関数定義時に1回だけ初期化されるためである.
次に、Pythonのこの特性を証明する例を示します.
class Test(object):  
    def __init__(self):  
        print("Init Test")  
          
def arg_init(a, b = Test()):  
    print(a)  

arg_init(1)  
arg_init(3)  
arg_init(5)  

結果:
Init Test
1
3
5

この例の結果から,Testクラスは一度だけインスタンス化された,すなわちデフォルトパラメータは関数呼び出し回数に関係なく,関数定義時に一度だけ初期化された.
可変デフォルトパラメータの正しい使用
可変のデフォルトパラメータでは、次のモードを使用して、予想外の結果を回避できます.
def append_item(a = 1, b = None):
    if b is None:
        b = []
    b.append(a)
    print b
    
append_item(a=1)
append_item(a=3)
append_item(a=5)  

結果:
[1]
[3]
[5]

Pythonにおける役割ドメイン
Pythonの役割ドメイン解析順序は,Local,Enclosing,Global,Built-in,つまりPython解釈器がこの順序で変数を解析する.
簡単な例を見てみましょう
global_var = 0

def outer_func():
    outer_var = 1
    
    def inner_func():
        inner_var = 2
        
        print "global_var is :", global_var
        print "outer_var is :", outer_var
        print "inner_var is :", inner_var
        
    inner_func()
    
outer_func()

結果:
global_var is : 0
outer_var is : 1
inner_var is : 2

Pythonでは、1つの役割ドメインで変数に値を割り当てると、Pythonはこの変数が現在の役割ドメインのローカル変数であると考えられることに注意してください.
この点も比較的分かりやすく、var_funcにはnum変数が割り当てられているので、ここでのnumvar_funcドメインのローカル変数である.
num = 0

def var_func():
    num = 1
    print "num is :", num
    
var_func()

問題1
しかし、変数を次のように使用すると、問題が発生します.
num = 0

def var_func():
    print "num is :", num
    num = 1
    
var_func()

その結果、このエラーが発生したのは、var_funcnum変数に値を付けたため、Python解釈器はnumvar_funcの役割ドメインのローカル変数であると考えているが、コードがprint "num is :", num文に実行されるとnumはまだ定義されていない.
UnboundLocalError: local variable 'num' referenced before assignment

問題2
上のエラーはまだ明らかで、もう一つの隠れたエラー形式は以下の通りです.
li = [1, 2, 3]

def foo():
    li.append(4)
    print li
    
foo()

def bar():
    li +=[5]
    print li
    
bar()

コードの結果は次のとおりです.
[1, 2, 3, 4]
UnboundLocalError: local variable 'li' referenced before assignment
foo関数では、Pythonの役割ドメイン解析順序に従って、この関数にはグローバルなli変数が使用される.しかし、bar関数では、li変数が付与されているため、libarの役割ドメインの変数として扱われる.bar関数のこの問題については、globalキーワードを使用することができる.
li = [1, 2, 3]

def foo():
    li.append(4)
    print li
    
foo()

def bar():
    global li
    li +=[5]
    print li
    
bar()

クラス属性の非表示
Pythonには、クラス属性とインスタンス属性があります.クラス属性はクラス自体に属し、すべてのクラスインスタンスによって共有されます.
クラスプロパティは、クラス名でアクセスおよび変更できます.また、クラスインスタンスでアクセスおよび変更できます.ただし、インスタンスがクラスと同じ名前のプロパティを定義すると、クラスプロパティは非表示になります.
次の例を見てください.
class Student(object):
    books = ["Python", "JavaScript", "CSS"]
    def __init__(self, name, age):
        self.name = name
        self.age = age
    pass
    
wilber = Student("Wilber", 27)
print "%s is %d years old" %(wilber.name, wilber.age)

print Student.books
print wilber.books

wilber.books = ["HTML", "AngularJS"]

print Student.books
print wilber.books

del wilber.books

print Student.books
print wilber.books

コードの結果、wilberインスタンスは、最初はクラスのbooksプロパティに直接アクセスできたが、インスタンスwilberbooksという名前のインスタンスプロパティを定義した後、wilberインスタンスのbooksプロパティはクラスのbooksプロパティを「非表示」にした.wilberインスタンスのbooksプロパティが削除されると、wilber.booksはクラスのbooksプロパティに対応します.
Wilber is 27 years old
['Python', 'JavaScript', 'CSS']
['Python', 'JavaScript', 'CSS']
['Python', 'JavaScript', 'CSS']
['HTML', 'AngularJS']
['Python', 'JavaScript', 'CSS']
['Python', 'JavaScript', 'CSS']

Python値で継承を使用する場合も、クラス属性の非表示に注意してください.1つのクラスについては、クラスの__dict__属性によってすべてのクラス属性を表示できます.
クラス名でクラス属性にアクセスすると、まずクラスの__dict__属性が検索され、クラス属性が見つからない場合は親が検索され続けます.ただし、子クラスが親と同じ名前のクラス属性を定義すると、子クラスのクラス属性は親クラスのクラス属性を非表示にします.
例を見てみましょう
class A(object):
    count = 1
    
class B(A):
    pass    
    
class C(A):
    pass        
    
print A.count, B.count, C.count      

B.count = 2

print A.count, B.count, C.count      

A.count = 3

print A.count, B.count, C.count     
print B.__dict__
print C.__dict__

その結果、クラスBcountというクラス属性を定義すると、親クラスのcount属性が非表示になります.
1 1 1
1 2 1
3 2 3
{'count': 2, '__module__': '__main__', '__doc__': None}
{'__module__': '__main__', '__doc__': None}

tupleは「可変」
Pythonではtupleは可変オブジェクトですが、ここでの可変とはtupleというコンテナ全体の要素が可変(正確には要素のid)ではありませんが、要素の値は変更できます.
tpl = (1, 2, 3, [4, 5, 6])

print id(tpl)
print id(tpl[3])

tpl[3].extend([7, 8])

print tpl
print id(tpl)
print id(tpl[3])

コード結果は、tplオブジェクトの各要素は可変であるが、tpl[3]listオブジェクトである.すなわち、このtplオブジェクトについては、id(tpl[3])は可変ではないが、tpl[3]は確かに可変である.
36764576
38639896
(1, 2, 3, [4, 5, 6, 7, 8])
36764576
38639896

Pythonの深浅コピー
Pythonオブジェクトに値を付ける操作では、オブジェクトの奥行きコピーに注意し、うっかり穴を踏む可能性があります.
次の操作を使用すると、浅いコピーの効果が得られます.
スライス[:]操作を使用
工場関数(list/dir/setなど)を使用する
copyモジュールのcopy()関数を使用
copyモジュールの浅いコピー関数copy()を使用します.
import copy

will = ["Will", 28, ["Python", "C#", "JavaScript"]]
wilber = copy.copy(will)

print id(will)
print will
print [id(ele) for ele in will]
print id(wilber)
print wilber
print [id(ele) for ele in wilber]

will[0] = "Wilber"
will[2].append("CSS")
print id(will)
print will
print [id(ele) for ele in will]
print id(wilber)
print wilber
print [id(ele) for ele in wilber]

copyモジュールの深いコピー関数deepcopy()を使用します.
import copy

will = ["Will", 28, ["Python", "C#", "JavaScript"]]
wilber = copy.deepcopy(will)

print id(will)
print will
print [id(ele) for ele in will]
print id(wilber)
print wilber
print [id(ele) for ele in wilber]

will[0] = "Wilber"
will[2].append("CSS")
print id(will)
print will
print [id(ele) for ele in will]
print id(wilber)
print wilber
print [id(ele) for ele in wilber]

モジュールサイクル依存
Pythonでimportを使用してモジュールを導入する場合、モジュールループ依存が発生する場合があります.例えば、次の例では、module_xモジュールとmodule_yモジュールが互いに依存し、module_y.pyを実行するとエラーが発生します.
# module_x.py
import module_y
    
def inc_count():
    module_y.count += 1
    print module_y.count
    
    
# module_y.py
import module_x

count = 10

def run():
    module_x.inc_count()
    
run()            

実際には、符号化の過程でサイクル依存を回避したり、コード再構成の過程でサイクル依存を解消したりしなければならない.
もちろん、上記の問題も解決できます.よく使われる解決策は、引用関係を明らかにし、あるモジュールを本当に必要なときに導入させることです(一般的に関数に入れます).
上記の例では、module_x.pyを次のように変更して、関数内部にmodule_yをインポートできます.
# module_x.py
def inc_count():
    import module_y
    module_y.count += 1
    print module_y.count    http://www.cnblogs.com/wilber2013/p/5178620.html