Pythonの入門者向け動画教材

去年から手伝っていて、今年の4月には公開されていたのですが、Pythonの入門者向けの動画の教材を作っていました。

JMOOCというオンライン講座の1つとして公開されています。 www.fisdom.org 講座はFisdomというプラットフォームで配信されているため、利用には登録が必要ですが、受講料はかかりません。

CMSコミュニケーションズの寺田さんと2人で半分ずつ分担してやってました。

Pythonを学習するための手助けになればいいなと思います。

DjangoでLOGGING設定をしているときに、IPythonの補完に不具合が出る場合の対処

Djangoに限ったことではないのですが、PythonのloggingでrootのハンドラをDEBUGにしていると、IPythonの補完を実行した際にparsoという依存ライブラリのログが出てしまい、カーソル位置がずれてしまうことあります。

例えば、Djangoで以下のように settings.py でロギングの設定をした場合。

LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'handlers': {
        'console': {
            'level': 'DEBUG',
            'class': 'logging.StreamHandler',
        },
    },
    'root': {
        'handlers': ['console'],
        'level': 'DEBUG',
    }
}

Djangomanage.py shell では、IPythonがインストールされていると、IPythonが起動します。この設定で補完機能を使おうとTabキーを押したとき、次のようにログが出力されてカーソル位置がずれます。

f:id:nullpobug:20190623164234p:plain
IPythonの補完時にログが出力される

parsoというモジュール(ipython → jedi → parsoの依存)がデバッグログを出力するのですが、これでは補完がまともに使えなくなってしまいます。

parsoのログをINFOレベルに設定することで、回避できます。(もしくは disable_existing_loggers やrootロガーの設定変更で回避することもできます)

LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'handlers': {
        'console': {
            'level': 'DEBUG',
            'class': 'logging.StreamHandler',
        },
    },
    'loggers': {
        'parso': {
            'handlers': ['console'],
            'level': 'INFO',
            'propagate': False,
        },
    },
    'root': {
        'handlers': ['console'],
        'level': 'DEBUG',
    }
}

参考

オープンソースカンファレンス2019 HokkaidoでDjangoの紹介をしました

6/1に札幌で開催された オープンソースカンファレンス2019 Hokkaido で、django-ja名義でDjangoフレームワークの紹介をしてきました。

資料はSpeakerDeckにアップロードしています。

Djangoフレームワークの紹介_OSC北海道2019 - Speaker Deck

日本のDjangoユーザーのコミュニティである django-ja では、Djangoフレームワークに関する雑談や、DjangoCongressJPの開催、Djangoフレームワークのドキュメントやリソースの翻訳を行っています。

Home | djangoproject.jp

DjangoのSubqueryとOuterRefを使ってサブクエリを組み立てる

DjangoのORMで、サブクエリを使う方法について。任意のSQLであればrawメソッドを使えばよいのですが、なるべくORMのAPIを使いたい。

DjangoのORMでは任意の位置にサブクエリを使えるわけではないですが、例えば「テーブル単位での問い合わせ結果にサブクエリで得た列を追加する」ぐらいのことは、annotateメソッドを使ってできます。

サブクエリ側で条件を指定した絞り込みを行う場合、SubqueryとOuterRefを組み合わせるとできます。

Query Expressions | Django documentation | Django

モデル

Categoryモデルを親に持つ、Itemモデル、という関係性のデータです。

from django.db import models
from django.utils import timezone


class Category(models.Model):
    parent = models.ForeignKey(
        'myapp.Category', null=True, blank=True,
        on_delete=models.CASCADE)
    name = models.CharField(max_length=20)

    class Meta:
        db_table = 'category'
        ordering = ['id']

    def __str__(self):
        return self.name


class Item(models.Model):
    category = models.ForeignKey(
        'myapp.Category', null=True, blank=True,
        on_delete=models.SET_NULL)
    name = models.CharField(max_length=20)
    updated = models.DateTimeField(default=timezone.now)

    class Meta:
        db_table = 'item'
        ordering = ['id']

    def __str__(self):
        return self.name

Djangoのシェルで試したコード

CountなどのAggregate Expressionでうまくクエリを作れない場合、Subqueryを継承すると、自由にクエリを作れます。

サブクエリの結果をCountするCountQueryクラスを定義して使ってます。

from datetime import timedelta
from django.utils import timezone
from django.db.models import OuterRef, Subquery, IntegerField
import sqlparse
from myapp.models import Category, Item

class CountQuery(Subquery):
    """件数をカウントするサブクエリ
    """
    template = "(SELECT COUNT(*) FROM (%(subquery)s) _count)"
    output_field = IntegerField()

categories = Category.objects.annotate(
    updated_count=CountQuery(
        Item.objects.filter(
            category_id=OuterRef('pk'),
            updated__gt=timezone.now() - timedelta(days=1)  # 24時間以内に更新されたItemの件数を取得
        )
    )
)

print(sqlparse.format(str(categories.query), reindent=True))  # sqlparseでSQL文を整形

組み立てたSQLは、sqlparseで整形して表示してみてます。

SELECT "category"."id",
       "category"."parent_id",
       "category"."name",

  (SELECT COUNT(*)
   FROM
     (SELECT U0."id",
             U0."category_id",
             U0."name",
             U0."updated"
      FROM "item" U0
      WHERE (U0."category_id" = ("category"."id")
             AND U0."updated" > 2019-03-19 08:22:03.638946)
      ORDER BY U0."id" ASC) _count) AS "updated_count"
FROM "category"
ORDER BY "category"."id" ASC

理想はもう1段減らしたいのだけど、きれいに作る方法を見つけれてないです...

参考

Django 1.11 Annotating a Subquery Aggregate - Stack Overflow

Djangoのマイグレーションでスキーマ変更とデータ変更を分けたほうがよいか

Djangoマイグレーションは、1つのマイグレーションで複数のタスクを実行できます。

スキーマを変更する処理のあとに、データのマイグレーションを実行、といったようなものです。

意味のある変更をまとめるのは便利そうに思えるのですが、落とし穴があったりします。

例を挙げてみます。

コード

myapp/models.py (モデル)

from django.db import models


class Item(models.Model):
    col1 = models.CharField(max_length=30)
    col2 = models.CharField(max_length=30, blank=True, null=True)  # このカラムを追加するマイグレーション

myapp/migrations/0002_item_col2.py (マイグレーションコード)

from django.db import migrations, models


def migrate_data(apps, schema_editor):
    Item = apps.get_model('myapp', 'Item')
    db_alias = schema_editor.connection.alias
    Item.objects.all().update(col2=models.F('col1'))
    raise Exception("ここでエラー発生")


class Migration(migrations.Migration):

    dependencies = [
        ('myapp', '0001_initial'),
    ]

    operations = [
        migrations.AddField(
            model_name='item',
            name='col2',
            field=models.CharField(blank=True, max_length=30, null=True),
        ),
        migrations.RunPython(
            migrate_data,
            migrations.RunPython.noop
        )
    ]

実行例

このコードは意図的にマイグレーション中に例外を発生させていますが、実際にはマイグレーションコードが何らかの例外を発生させて落ちる状況を考えてください。

1回目の実行では、想定通りExceptionが発生して停止します。

Exception: ここでエラー発生

2回目の実行では、データベースにSQLite3を使っている場合は、1回目同様、Exceptionで落ちますが、MySQLを使っている場合はスキーマ変更のほうでエラーになります。

django.db.utils.OperationalError: (1060, "Duplicate column name 'col2'")

これは、SQLite3やPostgreSQLスキーマ変更もトランザクション内で実行し、ロールバックできるので、何度実行しても同じになりますが、MySQLOracleスキーマ変更はトランザクションの対象外だからです。

MySQLの場合はスキーマ変更をして、データ更新に失敗した場合、スキーマ変更はロールバックされません。

この例だとカラムが残ったままま、マイグレーション失敗、という扱いになります。リカバリには手動でカラムを削除してやりなおす必要があります。

結論

スキーマ変更とデータ変更のマイグレーションは、同じクラスにまとめずに分けておいたほうが、失敗時のリカバリが楽です。

参考

GoogleAppEngine上で動作しているDjangoアプリをStackdriver Debuggerでデバッグする

AppEngineで動かしてるDjangoアプリをStackdriver Debuggerでデバッグする手順について。

Python 用 Stackdriver Debugger の設定  |  Stackdriver Debugger のドキュメント  |  Google Cloud

ドキュメントには、Django 1.Xの話しかなく、手順も良くなかったのでメモを残します(フィードバックはしておきました)。

Python 3.7、Django 2.1.4、AppEngineはStandard、Flexible両方で試しました。

requirements.txtへの追記

インストールするライブラリは、ドキュメント通り google-python-cloud-debugger で大丈夫でした。

requirements.txtgoogle-python-cloud-debugger をインストールするように追記しておきます。

google-python-cloud-debugger

Djangoの設定

DjangoアプリをAppEngineで動かす場合、エントリポイントは wsgi.py になります(gunicorn等でwsgi.pyを読み込ませてる場合)。

mysite という名前でプロジェクトを作った場合、 mysite/wsgi.py にStackdriver Debuggerを有効にするコードを追記します。

import os
from django.core.wsgi import get_wsgi_application

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mysite.settings')
application = get_wsgi_application()
# Enable Stackdriver Debugger(以下が追記部分)
try:
    import googleclouddebugger
    googleclouddebugger.enable()
except ImportError:
    pass

デバッグしてみる

変更したものをAppEngineにデプロイ後、GCPのコンソールからStackdriverデバッグの画面を開いてみます。

ソースコードが表示されない場合は、デプロイ時のコマンドを gcloud beta app deploy (betaコマンド)にしてみると改善するかもしれません。

ソースコードが表示されたら、デバッグしたい行にスナップショットのポイントや条件、式を指定し、実際にブラウザでアプリケーションを操作します。

指定したスナップショットのポイントを通過すると、デバッグの画面で、変数やコールスタックなどを確認できます。

f:id:nullpobug:20190115145321p:plain

本番環境を気軽にデバッグできるので便利ですね。

GoogleAppEngine上でDjangoフレームワークのログをStackdriver Loggingに流す

PythonのloggingモジュールからStackdriverにログを流す手順はドキュメントに書かれている。

Setting Up Stackdriver Logging for Python  |  Stackdriver Logging  |  Google Cloud

Djangoフレームワークを使っている場合は、Djangoのロギング設定を考慮する必要がある。

ちゃんと設定しておくと、logging.infoやlogging.errorで出力したレベルの情報をStackdriver上でも付与された状態でログを見れる。

f:id:nullpobug:20181222074919p:plain

Python 3.7、Django 2.1.4、google-cloud-logging 1.9.1で試した。

Djangoの設定

あらかじめ、対象のプロジェクトでStackdriver LoggingのAPIを有効にしておく。

また、クライアントライブラリ( google-cloud-logging ) をインストールしておく。

Djangosettings.py に追記する想定。

from google.cloud import logging as google_cloud_logging

log_client = google_cloud_logging.Client()

LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'handlers': {
        'stackdriver_logging': {
            'class': 'google.cloud.logging.handlers.AppEngineHandler',
            'client': log_client,
        },
    },
    'loggers': {
        'django': {
            'handlers': ['stackdriver_logging'],
            'level': 'INFO',
            'propagate': True,
        },
        'django.request': {
            'handlers': ['stackdriver_logging'],
            'level': 'ERROR',
        },
    },
    'root': {
        'handlers': ['stackdriver_logging'],
        'level': 'INFO',
    }
}
  • AppEngineのデフォルトのサービスアカウントで書き込みは可能なので認証の設定は不要
  • ドキュメントにある setup_logging() は、呼ばないでおく(Django側でロギングのセットアップが走るので不要)
  • rootロガーの設定は好み
  • AppEngineを使っている場合のハンドラは、 google.cloud.logging.handlers.AppEngineHandler を使っておくとFlexible/Standardどちらでも都合が良いらしい
  • gunicornのログも流そうとすると、いろいろややこしいのでDjangoのみにしてある

参考