【Flask】check_password_hash関数でAttributeError: Neither 'InstrumentedAttribute' object nor 'Comparator' object associated with User.password has an attribute 'split'が発生する

check_password_hash関数の箇所でなぜかエラーになり解決に時間を要しました。
忘れないうちに、備忘として残しておこうと思います。

## 環境
docker 24.0.6
docker-compose 2.23.0
python 3.13.0
Flask 3.1.0
nginx 1.27.4
gunicorn 23.0.0
PostgreSQL 17.3

環境はdockerを用いてnginxのwebサーバーコンテナ、gunicorn+flaskのアプリケーションサーバーコンテナ、PostgreSQLのDBコンテナを構築しています。
こちらの記事を参考にしました。

https://qiita.com/usaitoen/items/bea738fa210216c5ea65

## ソースコード
```views.py
@login_bp.route("/login", methods=["GET", "POST"])
def login():
    form = LoginForm(request.form)
    if request.method == "GET":
        return render_template("user_account/login.html", form=form)
    elif request.method == "POST" and form.validate_on_submit():
        user = User.select_by_emial(form.email.data)
        if user and user.check_password(form.password.data):
            login_user(user, remember=True, duration=timedelta(seconds=600))
            flash("ログインしました")
            return redirect(url_for("user.user"))
        elif user == None:
            flash("ユーザーが存在しません")
            return render_template("user_account/login.html", form=form)
```

```models.py
class User(UserMixin, db.Model):
    __tablename__ = "users"
    
    id = Column("id", Integer, primary_key=True, autoincrement=True)
    username = Column("username", String(255), nullable=False)
    email = Column("email", String(255), nullable=False, unique=True)
    age = Column("age", Integer)
    password = Column("password", String(255), nullable=False)

    (中略)

    @classmethod
    def check_password(self, password):
        return check_password_hash(self.password, password)
```

原因となった箇所のみ抜粋です。
具体的にはmodels.py内の一番最後の行のcheck_password_hash(self.password, password)でエラーが起きました。

## 事象
1. signupページでユーザーを登録します。なんとも簡素ですね。
パスワードは'password'で登録しています。
![sinuppage.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1091995/0e8d5b19-fec6-4fef-b499-423f89bdda01.png)
テーブルに入っていることを確認できました。
![スクリーンショット 2025-02-17 114222.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1091995/0b6e9636-17d2-45f6-81c9-ace38a4823c5.png)

2.loginページでログインします。なんとも簡素ですね。
![スクリーンショット 2025-02-17 115613.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1091995/8cde7f92-dec3-4d83-97c3-3ecbb52d9760.png)

3.check_password_hash関数でAttributeErrorが発生しました。なんで?
![スクリーンショット 2025-02-17 115918.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1091995/b726829d-4ca9-4708-90f3-c2f6d6eba6e1.png)


## 調査
エラーはAttributeError: Neither 'InstrumentedAttribute' object nor 'Comparator' object associated with User.password has an attribute 'split'と吐いています。要するに、ユーザーパスワードに関連付けられたオブジェクトがsplitメソッドを持ってないよってことです。

check_password_hash関数の中身では、引数で渡されてきたpwhashに対してsplitを行っています。splitが使えるということはpwhashはおそらく文字列ですね。

![split.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1091995/aa2ab29f-5fe9-4748-a6e0-cc767ba2d486.png)


該当箇所付近をデバッグしてみます。
printでそれぞれの値と型を確認します。
まずはviews.pyから。

```views.py
@login_bp.route("/login", methods=["GET", "POST"])
def login():
    form = LoginForm(request.form)
    if request.method == "GET":
        return render_template("user_account/login.html", form=form)
    elif request.method == "POST" and form.validate_on_submit():
        user = User.select_by_emial(form.email.data)

        print(type(user.password))#user.passwordの型
        print(user.password)#user.passwordの値(ハッシュ化されたデータ)

        print(type(form.password.data))#form.password.dataの型
        print(form.password.data)#form.password.dataの値

        if user and user.check_password(form.password.data):
            login_user(user, remember=True, duration=timedelta(seconds=600))
            flash("ログインしました")
            return redirect(url_for("user.user"))
        elif user == None:
            flash("ユーザーが存在しません")
            return render_template("user_account/login.html", form=form)
    
    return render_template("user_account/login.html", form=form)
```


![ターミナル.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1091995/774cc34f-bdd4-463c-89e3-2d5b417054a8.png)


ターミナルに出力された値を確認するとどちらもstring(文字列型)です。
user.passwordもハッシュ化されたデータが入ってますし、form.password.dataも入力した'password'という文字列が入っています。
どちらも問題ありません。

次に、models.pyのcheck_password_hash付近をデバッグします。
コードとデバッグ結果がこちらです。

```models.py
    (中略)
    
    @classmethod
    def check_password(self, password):

        print(type(self.password))
        print(self.password)
        print(type(password)) #引数として渡されてきたパスワードの型
        print(password) #引数として渡されてきたパスワードの値

        return check_password_hash(self.password, password)
```

![ターミナル2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1091995/5efe9f9a-1d3b-4793-b9b2-a23da026cba2.png)

引数で渡されてきたパスワードの方は型も値も一致してるけど、self.passwordの方はインスタンス変数password取得できてなくない???


## 原因特定
小一時間悩みましたが、check_password_hash関数にデコレータの@classmethodがついた状態なので、クラスメソッドとしての振る舞いになっていたことが原因でした。多分ほかの関数からコピペしてきたときに一緒に持ってきちゃって消し忘れたのだと思います。普通に見落としです...

```diff_python
    (中略)
    
-   @classmethod
    def check_password(self, password):
        return check_password_hash(self.password, password)
```

@classmethodをコメントアウトして実行すると、クラスではなくインスタンスとして機能して想定通りの結果になりました!!!

![最後.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1091995/a242a303-41e3-4999-ace4-3f3296add7d9.png)

HAPPY