PythonでUnboundLocalErrorに遭遇したら

2024.03.31

こんちには。

データアナリティクス事業本部 機械学習チームの中村(nokomoro3)です。

本記事ではPythonの変数スコープと、UnboundLocalErrorについて整理したいと思います。

はじめに

Pythonでは、関数外の変数であっても、関数内で以下のように参照することができます。

var1 = "関数外の変数"

def print_var1():
    print(f"{var1=}")
    return

print_var1()
var1='関数外の変数'

上記のようにインデントの最も浅い部分に定義された変数は、グローバル変数となります。

以下のコードでそれをチェックしてみましょう。

var1 = "関数外の変数"
print(f"{var1=}, {id(var1)=}")

def print_var1():

    # globalかどうかを確認
    if "var1" in globals().keys():
        print("var1 is global")
    else:
        print("var1 is not global")

    print(f"{var1=}, {id(var1)=}")

    return

print_var1()
print(f"{var1=}, {id(var1)=}")
var1='関数外の変数', id(var1)=135576734702416
var1 is global
var1='関数外の変数', id(var1)=135576734702416

var1はグローバル変数であり、オブジェクトIDも同じものとなっていることが分かります。

グローバル変数を書き換えようとすると

ただし、例えば以下のように参照しつつ再代入しようとするとエラーとなります。

var1 = "グローバル変数"
print(f"{var1=}, {id(var1)=}")

def print_var1_with_concat():

    var1 = var1 + "_結合したい文字列"
    print(f"{var1=}, {id(var1)=}")

    return

print_var1_with_concat()
print(f"{var1=}, {id(var1)=}")
var1='グローバル変数', id(var1)=135576734933104
---------------------------------------------------------------------------
UnboundLocalError                         Traceback (most recent call last)
<ipython-input-41-3041b5379d6e> in <cell line: 11>()
      9     return
     10 
---> 11 print_var1_with_concat()
     12 print(f"{var1=}, {id(var1)=}")

<ipython-input-41-3041b5379d6e> in print_var1_with_concat()
      4 def print_var1_with_concat():
      5 
----> 6     var1 = var1 + "_結合したい文字列"
      7     print(f"{var1=}, {id(var1)=}")
      8 

UnboundLocalError: local variable 'var1' referenced before assignment

これは関数内でvar1がローカル変数と見なされるようになるためです。

こういったUnboundLocalErrorに遭遇する機会は意外と経験があることかと思います。

ローカル変数を定義しなおすと

こちらは関数内で変数を定義しなおすとエラーがなくなります。

var1 = "グローバル変数"
print(f"{var1=}, {id(var1)=}")

def print_var1_with_concat():

    var1 = "関数内の変数"
    var1 = var1 + "_結合したい文字列"
    print(f"{var1=}, {id(var1)=}")

    return

print_var1_with_concat()
print(f"{var1=}, {id(var1)=}")
var1='グローバル変数', id(var1)=135576734702528
var1='関数内の変数_結合したい文字列', id(var1)=135576734703088
var1='グローバル変数', id(var1)=135576734702528

しかしこれは元のグローバル変数を変更するわけではありません。

グローバル変数は書き変わっておらず、オブジェクトIDも関数内と関数外で異なることが分かります。

つまり、グローバル変数と同じ変数名のものを、ローカル変数として関数内で定義すると、ローカル変数と見なされていることが分かります。

ここまでの話を整理すると

先ほどのエラーとなるコードではvar1 = var1 + "_結合したい文字列"という一行により、ローカル変数としての定義と参照を同時に行っています。

また定義前に参照が実行されるため、UnboundLocalErrorが発生しているということとなります。

つまり、グローバル変数と同じ名前の変数を関数内でローカル変数として使用すると、ローカル変数と見なされ、ローカル変数と見なされたものを、定義前に参照しようとすることでエラーとなっています。

よって以下のようなコードも、もちろんエラーとなります。

var1 = "グローバル変数"
print(f"{var1=}, {id(var1)=}")

def print_var1_with_concat():

    print(f"{var1=}, {id(var1)=}") # ここでエラー

    var1 = "関数内の変数"
    var1 = var1 + "_結合したい文字列"
    print(f"{var1=}, {id(var1)=}")

    return

print_var1_with_concat()
print(f"{var1=}, {id(var1)=}")
var1='グローバル変数', id(var1)=135576734933584
---------------------------------------------------------------------------
UnboundLocalError                         Traceback (most recent call last)
<ipython-input-42-7e829d20b7e6> in <cell line: 14>()
     12     return
     13 
---> 14 print_var1_with_concat()
     15 print(f"{var1=}, {id(var1)=}")

<ipython-input-42-7e829d20b7e6> in print_var1_with_concat()
      4 def print_var1_with_concat():
      5 
----> 6     print(f"{var1=}, {id(var1)=}") # ここでエラー
      7 
      8     var1 = "関数内の変数"

UnboundLocalError: local variable 'var1' referenced before assignment

グローバル変数を書き換えるには

先ほどのコードではグローバル変数は書き換えができなかったですが、グローバル変数を書き換えたい場合は(良いかどうかは置いておいて)、以下のようにglobal 変数名と書くことで実現できます。

var1 = "グローバル変数"
print(f"{var1=}, {id(var1)=}")

def print_var1_with_concat():

    global var1
    var1 = var1 + "_結合したい文字列"
    print(f"{var1=}, {id(var1)=}")

    return

print_var1_with_concat()
print(f"{var1=}, {id(var1)=}")
var1='グローバル変数', id(var1)=135576734703200
var1='グローバル変数_結合したい文字列', id(var1)=135576734703312
var1='グローバル変数_結合したい文字列', id(var1)=135576734703312

ちなみに、最初とオブジェクトIDが変わるのは再代入により新しい変数となっているだけで、str型がimmutableなためであるため、今回の変数スコープの話とは無関係となります。

またmutableなデータ型の場合、global 変数名などと記述しなくとも参照さえできれば書き換えが可能となります。

(mutable、immutableについては機会があれば別途記事にしようと思います)

関数内の関数の場合もエラーとなりうる

これらの挙動はグローバル変数だけではなく、関数内の関数で、その親関数の変数を参照する際にも発生します。

def wrapper():

    var1 = "wrapper内の変数"
    print(f"{var1=}, {id(var1)=}")

    def print_var1_with_concat():

        var1 = var1 + "_結合したい文字列"
        print(f"{var1=}, {id(var1)=}")

        return

    print_var1_with_concat()
    return

wrapper()
var1='wrapper内の変数', id(var1)=135576734703088
---------------------------------------------------------------------------
UnboundLocalError                         Traceback (most recent call last)
<ipython-input-36-86d9896b7e57> in <cell line: 16>()
     14     return
     15 
---> 16 wrapper()

1 frames
<ipython-input-36-86d9896b7e57> in print_var1_with_concat()
      6     def print_var1_with_concat():
      7 
----> 8         var1 = var1 + "_結合したい文字列"
      9         print(f"{var1=}, {id(var1)=}")
     10 

UnboundLocalError: local variable 'var1' referenced before assignment

wrapper内で定義された変数が、その中の関数でも変数として定義されているため、未定義前に参照しに行くこととなり、先ほどと同様にUnboundLocalErrorとなることがわかります。

親関数の変数を書き換えたい場合はnonlocal

このような場合に、親関数の変数を書き換えたい時は、global 変数の代わりにnonlocal 変数を記載する必要があります。

def wrapper():

    var1 = "wrapper内の変数"
    print(f"{var1=}, {id(var1)=}")

    def print_var1_with_concat():

        nonlocal var1
        var1 = var1 + "_結合したい文字列"
        print(f"{var1=}, {id(var1)=}")

        return

    print_var1_with_concat()
    print(f"{var1=}, {id(var1)=}")

    return

wrapper()
var1='wrapper内の変数', id(var1)=135576734702528
var1='wrapper内の変数_結合したい文字列', id(var1)=135576734652976
var1='wrapper内の変数_結合したい文字列', id(var1)=135576734652976

関数内の関数内の関数でnonlocalを使うと?

では関数内の関数内の関数でnonlocalを使うとどうなるか?気になるところですが、これはひとつ外側の変数を取ってくるという挙動のようです。

def wrapper_of_wrapper():

    var1 = "wrapperのwrapper内の変数"

    def wrapper():

        var1 = "wrapper内の変数"

        def print_var1_with_concat():

            nonlocal var1
            var1 = var1 + "_結合したい文字列"
            print(f"{var1=}, {id(var1)=}")

            return

        print_var1_with_concat()

        return

    wrapper()

wrapper_of_wrapper()
var1='wrapper内の変数_結合したい文字列', id(var1)=135576734651440

まとめ

いかがでしたでしょうか。本ブログがPythonの変数スコープやUnboundLocalErrorに遭遇した方の参考になれば幸いです。