超初歩的なミスですが、関連情報が見つからないので共有しておきます。
TL;DR
循環参照ダメ、絶対(CORS, Flaskに限らず)
linterを入れよう
やりたかっとこと・エラー
- フロント Next.js (※今回フロント側は一切関係ないです)
- バックエンド Flask
で、Next側からFlask側のAPIを叩きたいが、flask runで動かすとCORSエラー。
本番環境では動作するが、dev環境ではエラーになる。
そもCORSとは?
Cross Origin Resource Sharing(オリジン間リソース共有)。
通常はオリジン(ドメインやポート)が異なる外部サーバーからのリクエストは弾かれるが、CORSを設定することで選択的にリクエストを通すことが可能。
Flaskにおいては、flask_corsモジュールでCORS機能が提供されています。
ダメだったプログラム
簡素化しています。
app
├── api
│ ├── routes.py
│ └── services.py
├── __init__.py
└── config.py
from app import create_app
from flask_cors import CORS
app = Flask(__name__)
CORS(app, origins=[
FRONT_URI,
FROT_DEV_URI
], supports_credentials=True)
if __name__ == '__main__':
app.run(debug=True)
from . import api
@api.route("/news-summary", methods=["GET"])
def get_news_summary():
return hogehoge()
from flask import Blueprint
api = Blueprint('api', __name__)
from . import routes
原因
はい、循環参照です。
気づいたときはもうアホかと思いましたが、routes.pyと__init__.pyで相互にimportしているせいでおかしくなっていました。
from . import api
from . import routes
本番環境の場合とdev環境のCORS以外の機能は正常に動いていたので、余計に解決が遅れました。
参考: ChatGPTによる説明「Pythonにおいて循環importは未定義動作ですか?」
ChatGPT:
Pythonにおいて循環インポート(circular import)は**未定義動作(undefined behavior)**とは言いませんが、意図しないエラーやバグを引き起こす可能性があるため注意が必要な動作です。
循環インポートとは?
例えば、以下のようにモジュールAとBが互いにインポートし合っている状態です:
# module_a.py
from module_b import func_b
def func_a():
print("A")
func_b()
# module_b.py
from module_a import func_a
def func_b():
print("B")
func_a()
Pythonの挙動(仕様上の動作)
Pythonのインポートは実行時に行われ、モジュールは最初にインポートされた時点で一度だけ評価(=実行)されます。
循環インポートがあると、片方のモジュールの定義がまだ終わっていない状態でもう片方から参照されることがあり、その結果、AttributeError や ImportError が発生することがあります。
これは 未定義動作(undefined behavior) ではなく、Pythonの明示的な仕様の中で起こる定義済みの挙動ですが、開発者の意図と食い違いやすく、バグの温床になります。
対処方法
循環インポートが問題になる場合、以下のような対処が推奨されます:
インポートの位置を関数内に移動する:
def func_a():
from module_b import func_b
print("A")
func_b()
依存関係を見直し、設計を分離・整理する:
共通の機能を別モジュールに切り出すなど。
遅延インポート(lazy import)や型ヒントのための if TYPE_CHECKING を使う:
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from module_b import SomeClass
結論
Pythonにおける循環インポートは「未定義動作」ではなく、「仕様上は定義されているが、予期しないエラーを引き起こすリスクが高いため、避けるべき構造」です。コード設計や依存関係の整理を通じて、循環参照を避けることが推奨されます。
解決法
__init__.pyに切り分ける意味があまりない分量に感じたので、routes.pyに統合したら動きました。
api = Blueprint('api', __name__)
@api.route("/news-summary", methods=["GET"])
def get_news_summary():
return hogehoge()
たぶんもっとベターな構成はありますがとりあえず。
あと同じことを絶対に今後もやらかすので、戒めのためlinter(flake8)を導入しました。
所感
そもそも循環importでエラーが起きないことを知らなかったので、Pythonが嫌いな理由が一つ増えそう(でも好き)でしたが、linterがあるので耐えです。
何につけてもネットの記事を丸コピするのはよくないですね。