mizzsugar’s blog

Pythonで学んだことや読書録を書きます。

DjangoORMでウィンドウ関数を使おう

この記事はDjango Advent Calendar 2019の記事です。

Django Advent Calendar 2019 - Qiita

そして私の初のアドベントカレンダーです!

最近仕事でBigQueryで分析関数を使うことが多いので、

そのなかでもウィンドウ関数をDjangoのORMでも使えないかなと思い調べてみました。

ウィンドウ関数とは

データベース製品よっては分析関数と呼ぶところもあればウィンドウ関数と呼ぶところもあります。

この記事ではPostgreSQLを使います。PostgreSQLでは分析関数をウィンドウ関数と呼んでいるのでウィンドウ関数で統一します。

PostgreSQLの公式ドキュメントには下記のように説明されています。

ウィンドウ関数は現在の行に何らかとも関係するテーブル行の集合に渡って計算を行います。 これは集約関数により行われる計算の形式と似たようなものです。 とは言っても、通常の集約関数とは異なり、ウィンドウ関数の使用は単一出力行に行をグループ化しません。 行はそれぞれ個別の身元を維持します。 裏側では、ウィンドウ関数は問い合わせ結果による現在行だけでなく、それ以上の行にアクセスすることができます。

3.5. ウィンドウ関数

例えば、このようなテーブルがある場合。

テーブル定義

    Column     |  Type   | Collation | Nullable |               Default                
---------------+---------+-----------+----------+--------------------------------------
 id            | integer |           | not null | nextval('salaries_id_seq'::regclass)
 department_id | integer |           | not null | 
 salary        | integer |           | not null | 
 employee_id   | integer |           | not null | 
Indexes:
    "salaries_pkey" PRIMARY KEY, btree (id)
    "salaries_departm_f58eef_idx" btree (department_id)
    "salaries_employe_a009a1_idx" btree (employee_id)


通常の集約関数で部署ごとの平均給与を出すとこのようになります。

SELECT
    department_id,
    ROUND(AVG(salary), 0) as avg_salary
FROM
    public.salaries
GROUP BY
    department_id
ORDER BY
    avg_salary DESC


出力結果例

 department_id | avg_salary 
---------------+------------
            16 |    9737374
            12 |    9694656
            15 |    9647917
            11 |    9588957
             5 |    9338559
            14 |    9337349
             6 |    9123184
             8 |    9070103
            13 |    9063918
            10 |    9020534
             9 |    8948148
            20 |    8934615
             4 |    8929108
            18 |    8712230
            17 |    8695489
            19 |    8669903
             2 |    8596131
             3 |    8578168
             7 |    8551880
             1 |    6790198


平均給与を出すだけなら良いですが、一人ひとりの社員の給与とその社員がその部署の中で何番目に給与が高いのかも出したいとします。

出力結果例

 department_id | employee_id |  salary  | rank 
---------------+-------------+----------+------
             1 |         414 | 40000000 |    1
             1 |         255 | 30000000 |    2
             1 |         316 | 20000000 |    3
             2 |         342 | 60000000 |    1
             2 |         673 | 50000000 |    2
             2 |         359 | 40000000 |    3
             3 |         409 | 60000000 |    1
             3 |         476 | 20000000 |    2
             3 |         410 | 10000000 |    3


このような結果を出すには上記のような単純なクエリでは出力できないので工夫が必要です。

そういう要望がある時に、ウィンドウ関数を知っていると、ウィンドウ関数を使わない場合よりもシンプルなクエリで実現できるまたはクエリのコストを抑えられることが多々あります。

ウィンドウ関数を使うとこのようなクエリになります。


SELECT
    department_id,
    employee_id,
    salary,
    rank() OVER (PARTITION BY department_id ORDER BY salary DESC)
FROM public.salaries
;


ウィンドウ関数なしだとこうまります。ウィンドウ関数を使ったクエリに比べてやや複雑です。

SELECT
    salaries.department_id,
    salaries.employee_id,
    salaries.salary,
    (
        SELECT COUNT(*)
        FROM public.salaries as rank
        WHERE
            rank.department_id = salaries.department_id
            AND rank.salary > salaries.salary
    ) + 1,
    rank() OVER (PARTITION BY department_id ORDER BY salary DESC)
FROM public.salaries
;


また、コストもレコード数が10000件のテーブルに関して

ウィンドウ関数を使ったクエリだと1024だったのに対し

ウィンドウ関数なしのクエリだと2072289でした。およそ206倍です。


rank()を説明しますと、PARTITION BYで部署ごとに分け、ORDER BYで部署の中でsalaryが多い順に順番を振り分けます。

ウィンドウ関数のPARTITION BYやORDER BYでどのようにテーブルを振り分けているかについては、下記のBigQueryのドキュメントの図1がわかりやすいです。

BigQueryなので分析関数と書いていますが、PostgreSQLのウィンドウ関数も同じ仕組みです。

cloud.google.com


DjangoORMでWindow関数を使うには

実行環境

* Python 3.8.0
* PostgreSQL 11.4
* Django 2.2.6
* psycopg2-binary 2.8.4


今回使用するモデル(テーブル定義は先程記述したテーブルと同じになります)

models.py

from django.db import models


class Salary(models.Model):
    # 面倒がってFK貼っていませんが実運用ではemployees  departmentsテーブル作ってFK貼ると思います
    employee_id = models.IntegerField()
    department_id = models.IntegerField()
    salary = models.IntegerField()

    class Meta:
        db_table = 'salaries'
        indexes = [
            models.Index(fields=['employee_id']),
            models.Index(fields=['department_id']),
        ]
    
    def __str__(self):
        return f'{self.department_id}_{self.employee_id}'


ウィンドウ関数をORMで表現するには、annotateを使います。

先ほどのクエリをORMで表現すると、下記のようになります。

from django.db.models.functions import Rank
from django.db.models import F, Window

from employee.models import Salary

window =  {
     'partition_by': [F('department_id')],
     'order_by': F('salary').desc()
}


Salary.objects.annotate(
    rank=Window(expression=Rank(), **window)
).values(
    'department_id',
    'employee_id',
    'salary',
    'rank'
)


F()は、モデルのフィールドを表すオブジェクトです。この例の場合、partition_byのdepartment_idとorder_byのsalaryです。

F()式のいいところは、実際にデータベースから値を取り出してPythonのメモリに格納しなくてもモデルのフィールドを参照できるところです。

https://docs.djangoproject.com/ja/2.2/ref/models/expressions/#f-expressions


annotateでウィンドウ関数でどのような項目を出力したいかしていします。引数expressionにDenseRank()を入れると、変数windowで指定したpartition byとorder byの通りにランキングされます。


この例では、rankという名前のアトリビュートにランキングを出力するように書いています。

https://docs.djangoproject.com/ja/2.2/ref/models/database-functions/#denserank


valuesで出力するアトリビュートを指定します。

valuesを指定しないと、id, department_id, employee_id, salary, rankの全てが出力されます。


Django Debug Toolbarをインストールし、

python manage.py debugsqlshellで上記のSQLを実行すると、

ウィンドウ関数が利用されたことを確認できました!

SELECT "salaries"."id",
       "salaries"."employee_id",
       "salaries"."department_id",
       "salaries"."salary",
       RANK() OVER (PARTITION BY "salaries"."department_id"
                    ORDER BY "salaries"."salary" DESC) AS "rank"
FROM "salaries"

https://docs.djangoproject.com/ja/2.2/ref/models/expressions/#window-functions


おまけ

ウィンドウ関数を紹介しましたが、場合によってはウィンドウ関数で表現できるけれどもウィンドウ関数を使わないほうが良い場合があります。


例えば、社員の給与と一緒にその社員が所属している部署の平均給与を表示したい場合です。

下記のような出力結果を期待します。

 department_id | employee_id | salary  | avg_salary 
---------------+-------------+---------+------------
             4 |        1707 | 8999649 |    5492152
             9 |        8296 | 8999531 |    5575559
             1 |        4641 | 8999048 |    5468614
            17 |         222 | 8998863 |    5376142
            19 |        4686 | 8997529 |    5449444
            10 |        1768 | 8994513 |    5588093
            17 |        7013 | 8994161 |    5376142
            15 |        6940 | 8994098 |    5535015
            11 |         577 | 8992925 |    5566160
            10 |        8139 | 8992773 |    5588093
             3 |        7718 | 8992511 |    5595843


ウィンドウ関数を使うと、下記のようなSQLになります。


SQLクエリ

SELECT
    department_id,
    employee_id,
    salary,
    rank() OVER (PARTITION BY department_id ORDER BY salary DESC)
FROM public.salaries
;


ORMクエリ

from django.db.models import F, Window, Avg

from employee.models import Salary


window =  {
    'partition_by': [F('department_id')],
    'order_by': F('salary').desc()
}


Salary.objects.annotate(
    avg_salary=Window(expression=Avg('salary'), **window)
).values(
    'department_id',
    'employee_id',
    'salary',
    'avg_salary'
)


ウィンドウ関数を使わないと、こうなります。

SQLクエリ

SELECT
    salaries.department_id,
    salaries.employee_id,
    salaries.salary,
    salary_averages.avg_salary
FROM
    public.salaries
JOIN
    (
    SELECT
        department_id,
        ROUND(AVG(salary), 0) as avg_salary
    FROM
        public.salaries
    GROUP BY department_id
) as salary_averages
ON
    salaries.department_id = salary_averages.department_id
ORDER BY
    salaries.salary DESC
;


ORMクエリは諦めました...

こういうの見つけたのですが、普通にSQL書いたほうが楽だと思ってしまいました。↓

Self join with django ORM - Stack Overflow


部署ごとの平均給与を出すクエリまではできました。給与が高い順です。

from employee.models import Salary


Salary.objects.values('department_id').annotate(avg_salary=models.Avg('salary')).order_by('-salary')


10000件レコードを入れてEXPLAINでコストを比較したところ、

①のウィンドウ関数を使ったクエリだと1024になったのに対し ②のJOINで対応したクエリだと393になりました。

ウィンドウ関数は便利ですが、その時々で出力したい結果に応じて使い分けようという話でした!


次回は、xKxAxKxさんです!

よろしくお願いします。

「データアーキテクト(データ整備人)を”前向きに”考える会」参加レポート

ブログ枠はブログが書くまでが勉強会ということで書きました!

イベントページ

analytics-and-intelligence.connpass.com


「データアーキテクト(データ整備人)の概観とこれからの展望と課題」 しんゆう さん (フリーランス

発表資料

speakerdeck.com

概要

  • データエンジニアはデータを集めてデータレイクに入れる人。ログ、バッチ処理などをする

  • データエンジニアとアナリストの間にあるのは

  • データの抽出

  • ダッシュボードなどでデータを可視化
  • データの整理。イレギュラーなデータへの対応や仕様変更への対応や監視や正確性の担保

  • 抽出、集計、管理は雑用ではなく専門家として役割を確立させるべき -> しんゆうさんは「データ整備人」という風に名付けた

  • 分析の経験がないと抽出の依頼に対して提案ができない

  • データ抽出はエンジニア領域に近い部分はあるものの、それが本業ではないので非エンジニアが対応すべき

  • 「次にデータを使う人が速やかに業務に遂行できるようにデータを使いやすくする人」

感想

  • 懇親会での話や発表を聞く限り、エンジニアやアナリストがデータ整備人を兼務していてデータ整備業は彼らの本分ではないのでモヤモヤしているという意見が多かったので、データ整備専門チームがある今の現場は恵まれていると思った

  • 分析の経験が自分にはないので、適切な提案できるデータ整備人ではないのが痛いところ


「3社の事例から学ぶ!現場で使われるダッシュボードの作り方」 ゆずたそ さん

発表資料

speakerdeck.com

概要

  • メルカリをサポートしている

  • メルカリはデータ整備人のポジションを最近作った

  • ダッシュボードは運用設計してちゃんと運用開始後にも見直すべし

  • 5W1Hを掘り下げてダッシュボード作ろう

  • ダッシュボード自体に対してPDCAまわそう

  • 5W1Hは焦点を小さく絞ろう。このくらい細かい粒度で↓

    • Who - 経営陣(Aさん、Bさん、Cさん)
    • When - 毎週水曜日の16時から
    • Where - 会議室Aで
    • Why - サービス利用状況を知るために
    • What - 主導線UU率の推移を
    • How - 議事録テンプレのURL経由で見る
  • 社内でSlackをどのように扱っているかのダッシュボードの例ー>Slackbotでオペレータに月イチで知らせる。ちゃんと運用される仕組み作り。

  • 「誰が」「いつ」「どこで」使うのか説明できるダッシュボードにしよう。誰かがいつか使ってくれるかもしれないのは結局使われない。

  • データを可視化するのはビジネスを助けるため

感想

  • データの可視化によってビジネス側が問題の原因となりうる事象に気付き、ビジネスルールを改善するという事例があった。データの可視化によってビジネスが改善される良い事例だった。メディア広告の例↓

https://speakerdeck.com/yuzutas0/20191127?slide=32

  • 5W1Hがとても細かくて驚いたけれども、そのくらい具体的で焦点が絞られていた方がちゃんと使われるかもしれない

  • データ整備の目的はデータが意思決定やビジネスの改善に貢献できるようにすること、というのが伝わる発表だった


「テータ整備業でぶつかった5つの課題_テータ整備人に求められる3つのスキル」 shinaro iwai さん(株式会社オプト)

発表資料

speakerdeck.com

概要

  • データ整備業をしている

  • クライアントからの依頼、社内からの依頼から求められていること: 意思決定の示唆、意思決定するために必要なことのすり合わせ

  • 雑なデータ取得依頼ー>なんのためにデータがほしいのかしつこく聞く

  • Google Colaboratory使うとSQLの処理の順序が可視化されてレビューしやすい

https://colab.research.google.com/notebooks/welcome.ipynb?hl=ja

  • ノウハウが共有されなくて辛い->GitHubにクエリTipsを保存

  • データ整備人に必要なスキル

    • 課題抽出力。「これを出すことでどんなメリットがあるのか」「結果を用いて何をしようとしているのか」を考える能力。これがないとただのAPI
    • SQL。関数とか学ぼう
    • データ理解力。データの仕様、どのように取得され、どのように書こうされるか。SQLが使えることとデータの仕様を理解するのは別能力

感想

  • Google Colaboratory使ってレビューしてみたい(されてみたい)

https://speakerdeck.com/siwai/tetazheng-bei-ye-tehutukatuta5tufalseke-ti-tetazheng-bei-ren-niqiu-merareru3tufalsesukiru-sdyong?slide=24

  • 数値の正しさの感覚やデータ理解力が自分にはまだないので、もっとビジネスを知る必要があるのが自分の課題

  • 何のためにデータを使いたいのかをビジネス側とすり合わせることは、余計なことをしたくない自分たちのためにも、効果的にビジネスの改善をしたいビジネス側の人たちにとっても大事なこと


「サイエンス視点からのデータアーキテクト」 堀野将晴 さん (ヤフー株式会社)

発表資料

www.slideshare.net

  • データサイエンスではモデリング・分析のための前処理・可視化

  • モデリングからサービス実装までが1チーム。

  • 大きなデータなので前処理が必須だけれども時間もCPUも使って大変。共通データが必要

  • たくさんの部署と関わるのでコミュニケーション能力がとても大事

  • ドメイン知識も大事

  • データ開発運用をサービス側の開発に求めるのは失敗した。目標の違いやリソースが逼迫しているため。サイエンス側と協力して開発

  • ログ設計のルールは整備人と実装側の認識合わせが必要

  • 意図通りに使われないテーブル。中間テーブル作ったら大元のテーブルとジョインされた・・・。ー> 使われ方はよく確認してから作ろう

  • データ整備人で価値を出すには能動的に動くことと開発運用まで携わること

感想

  • データサイエンスに使われるデータための開発とサービス開発は根本的な目的が違うので、サービス側に任せずデータサイエンス側と開発するというのが印象的だっ た。サービス側でも開発できると思っていたので・・・

  • データ整備は誰もやりたがらないからこそ能動的に動くと価値があるというのもキャリアを考える上で参考になった


全体的なまとめ

  • データ整備業は雑用に思われがちだけれども他の役割と片手間で専門的な知識が必要な大事な役割

  • データ整備人に必要なスキル

    • 課題抽出力 - ビジネス上の課題を知ってビジネスの改善や意思決定に貢献できるようになろう
    • コミュニケーション能力 - データ取得を依頼するいろんな部署の人や外部の人と関わるため
    • ドメイン知識 - データがビジネスの改善や意思決定に使われるためにはそもそもビジネスを知っている必要がある

djangoでタイムゾーンとうまく付き合う

Djangogirls Tutorialで今まで何気なく書いていた、Postモデルのpublished_dateとcreated_dateで使うdjango.utils.timezone.now。これについて疑問に思ったことがあったので調べました。


対象

  • (レベル感)Djangoチュートリアルくらいの複雑さのものを自力で作ることができる

  • (問題)今のところ作りたいものは作れているけど正直タイムゾーンの扱いの仕組みをわかっていない人


1. shell上でPost.objects.get(pk=1).pulished_dateを出力するとUTC時間で出力されるけれども、テンプレートで描画したブラウザ上では日本時間でで出力されるのはなぜ?

Djangoでは、タイムゾーンサポートを有効にした場合(つまりsettings.pyでUSE_TZ=Trueとした場合、日時は

となります。

タイムゾーンについて、Pythonでは「native」と「aware」という概念があります。

以下、ざっくり、タイムゾーンなしの時間を「native」な時間、タイムゾーンありの時間を「aware」な時間とします。

nativeとawareについては下記の記事が分かりやすいです。

【Django】native timeをaware timeに変換する方法 | エンジニアの眠れない夜

公式ドキュメントはこちらです。

datetime --- 基本的な日付型および時間型 — Python 3.8.0 ドキュメント

例えば、Djangoでこんなモデルがあるとします。

models.py

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


class Post(models.Model):
    author = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE
    )
    title = models.CharField(max_length=200)
    text = models.TextField()
    created_date = models.DateTimeField(default=timezone.now)
    published_date = models.DateTimeField(blank=True, null=True)

    def publish(self):
        self.published_date = timezone.now()
        self.save()

    def __str__(self):
        return self.title

    class Meta:
        db_table = 'posts'


Djangoのshell機能で動きを確認すると・・・

>>> from django.contrib.auth.models import User
>>> from blog.models import Post
>>> 
>>> author = User.objects.create(username='dummy', email='dummy@example.com')
>>> post = Post.objects.create(
    author=author,
    title='dummy',
    text='dummy'
)


Postを日本時間で2019/10/10 19:00:00 に保存した場合、created_dateは下記のようになります。

  • データベース(PostgreSQL以外) 2019-10-10 10:00:00

  • データベース(PostgreSQL) 2019-10-10 10:00:00+00

Djangoでは、PostgreSQLだとdatetimeFieldはデータベース上ではtimestamp with time zoneとなり、タイムゾーンUTCとなる仕様です。

その他のデータベースだとタイムゾーンなしとなります。値はUTCの日時で保存されます。

# 上記省略
>>> post.created_date
datetime.datetime(2019, 10, 10, 10, 0, 0, tzinfo=<UTC>)

Djangoでは、タイムゾーンサポートを有効にした場合、datetimeオブジェクトはawareなオブジェクトとして扱われます。

内部的な処理にはタイムゾーンUTCのdatetimeオブジェクトを使い、 エンドユーザー入出力を行うレイヤー(テンプレートやフォームなど)では現地時間でやり取りするという思想のもと、このようになっています。

なぜフォームやテンプレートだけ現地時間で内部の処理はUTCかというと、 ユーザーが複数のタイムゾーンを使用しており、ユーザに対して彼らの時計と同じ日時を表示したい場合に内部はUTCにすると柔軟に時間の変換ができ、時間を扱うオブジェクトを処理する過程で値を間違えることを防げるからです。 また、UTCだとサマータイムを適用している場合に変換ミスが起こることを防ぎます。

とはいっても複数のタイムゾーン使っていないとイメージ湧きづらいと思うので、とりあえずDjangoではタイムゾーンを使うのがデフォルトなので よほどのことがない限りタイムゾーンサポートを有効にするべしということを抑えられれば、と思います。

詳しくは↓

https://docs.djangoproject.com/ja/2.2/topics/i18n/timezones/


2. 時間を検索条件に入れたい時、どんな形で時間のオブジェクトを渡すのが良いのだろう?

タイムゾーンを有効にしている場合、 タイムゾーンに指定したdatetimeオブジェクトかUTCタイムゾーンとしたdatetimeオブジェクトが良いと思います。 どちらかと言えばどちらでもいいというのが個人的な所感です笑

ケースバイケースですが、 DjangoのFormクラスのDatetimeFieldのcleaned_dataや、DRFのSerializerのDatetimeFieldのvalidated_dataでは 現地をタイムゾーンにしたdatetimeオブジェクトが出力されるので それらを使うならわざわざUTCに直してから使うこともないかなと思います。

forms.py

from django import forms


class SearchPostForm(forms.Form):
    from_date = forms.DateTimeField()
    to_date = forms.DateTimeField()


filterで検索してみる

>>> from blog.forms import SearchPostForm
>>> from blog.models import Post
>>>
>>>
>>> form = SearchPostForm({'from_date':'2019-10-10 10:00:00', 'to_date':'2019-10-10 11:00:00'})
>>> form.is_valid()
True
>>> form.cleaned_data
{'from_date': datetime.datetime(2019, 10, 10, 10, 0, tzinfo=<DstTzInfo 'Asia/Tokyo' JST+9:00:00 STD>), 'to_date': datetime.datetime(2019, 10, 10, 11, 0, tzinfo=<DstTzInfo 'Asia/Tokyo' JST+9:00:00 STD>)}
>>>
>>> from_date = form.cleaned_data.get('from_date')
>>> to_date = form.cleaned_data.get('to_date')
>>>
>>> Post.objects.filter(published_date__gte=from_date, published_date__lte=to_date)[0].published_date
datetime.datetime(2019, 10, 10, 1, 0, tzinfo=<UTC>)


serializers.py

from rest_framework import serializers

class SearchPostSerializer(serializers.Serializer):
    from_date = serializers.DateTimeField()
    to_date = serializers.DateTimeField()


filterで検索してみる

>>> from blog.serializers import SearchPostSerializer
>>> from blog.models import Post
>>>
>>>
>>> serializer = SearchPostSerializer(data={'from_date':'2019-10-10T10:00:00+09:00', 'to_date':'2019-10-10T11:00:00+09:00'})
>>> serializer.is_valid()
True
>>> serializer.validated_data
>>> serializer.validated_data
OrderedDict([('from_date', datetime.datetime(2019, 10, 10, 10, 0, tzinfo=<DstTzInfo 'Asia/Tokyo' JST+9:00:00 STD>)), ('to_date', dat
etime.datetime(2019, 10, 10, 11, 0, tzinfo=<DstTzInfo 'Asia/Tokyo' JST+9:00:00 STD>))])
>>>
>>> from_date = serializer.validated_data.get('from_date')
>>> to_date = serializer.validated_data.get('to_date')
>>>
>>> Post.objects.filter(published_date__gte=from_date, published_date__lte=to_date)[0].published_date
datetime.datetime(2019, 10, 10, 1, 0, tzinfo=<UTC>)


また、nativeなオブジェクトを使うとこんなwarningが出ます。

RuntimeWarning: DateTimeField received a naive datetime * to while time zone support is active.

Awareなオブジェクトを使う設定にしているのにnativeなの使うなよってことですね。

試しにnativeなオブジェクトで使ってみたところ、UTCとして扱われるような動きをしています。 Nativeオブジェクトだと意図しない検索結果になることがあるのでお勧めしません。


3. APIではどのように出力するのが良いだろう

テンプレートに時間を表示したい場合、自動でローカルタイムに変更されて表示されます。では、APIでは?

Postモデルの一覧を返したいとします。

Post.objects.get(pk=1)django.https.JsonResponse(またはHttpResponse)を使って返すと、UTCのままになります。

views.py

from django.forms.models import model_to_dict
from django.http import JsonResponse
from django.shortcuts import get_object_or_404
from django.views.decorators.http import require_http_methods

from blog.models import Post


@require_http_methods(['GET'])
def get(request, pk: int):
    post = get_object_or_404(Post, pk=pk)
    return JsonResponse(
        {
            'post': model_to_dict(post)
        }
    )


レスポンス

{"post": {"id": 1, "author": 1, "title": "not_published", "text": "dummy", "created_date": "2019-10-10T10:00:00Z", "published_date": null}}


現地時間でエンドユーザーに使ってもらいたい場合、JSを使ってフロント側で変換するか、view関数内で変換する必要があります。


① (JavaScriptを使ってエンドユーザーに時間を表示する場合)UTC時間でAPIは返してJavaScriptに現地時間に直してもらう

Moment.jsを使うとこんな感じになります。

Moment.js | Home

moment('2019-10-10T10:00:00.000Z').tz("Asia/Tokyo").format()
// ブラウザ上では`2019-10-10 19:00:00`となります。


フォーマットを変更するには

moment('2019-10-10T10:00:00.000Z').tz("Asia/Tokyo").format('YYYY年MM月DD日 HH時mm分SS秒’)
// ブラウザ上では` 2019年10月10日 19時00分00秒` となります。


タイムゾーンUTCのdatetimeオブジェクトをDjango.utils.timezone.localtimeで現地時間に変換する。

localtime()で現地をタイムゾーンにしたdatetimeオブジェクトに変換されます。

>>> from django.utils import timezone
>>>
>>>
>>> time = timezone.now()  # 2019-10-10 10:00:00 tzinfo=<UTC>とします
>>> timezone.localtime(time)
datetime.datetime(2019, 10, 10, 19, 00, 00, 00000, tzinfo=<DstTzInfo 'Asia/Tokyo' JST+9:00:00 STD>)


③ (DRFを使っている場合)Serializerに現地時間に返してもらう。

上記と同じです。Serializerのvalidated_dataをResponseに入れて返します。

serializers.py

from rest_framework import serializers

from blog.models import Post


class PostModelSerializer(serializers.ModelSerializer):
    class Meta:
        model = Post
        fields = ('author', 'title', 'text', 'published_date')
>>> from blog.models import Post
>>> from blog.serializer import PostSerializer
>>> 
>>> 
>>> post = Post.objects.get(pk=2)
>>> serializer = PostSerializer(post)
>>> serializer.data
{'author': 1, 'title': 'already_published', 'text': 'dummy', 'published_date': '2019-10-10T10:00:00+09:00'}
>>> # +09:00となっているのでタイムゾーンがAsia/Tokyoの時間とわかる。


なお、SerializerのDatetimeFieldはデフォルトでISO-8601フォーマットになっています。それ以外のフォーマットで出力したい場合、DatetimeFieldの引数formatを指定してください。

例えばこんな感じで

from rest_framework import serializers

serializers.DateTimeField(format="%Y-%m-%d %H:%M:%S %Z")


Formatのドキュメント↓

https://www.django-rest-framework.org/api-guide/fields/#datetimefield-format-strings


どちらでも良いと思いますが、DRFがよしなにやってくれているので3が間違いが少なく楽かなと個人的には思います。

ただ、いろんなAPIのドキュメントやFAQを見ていると、 どのタイムゾーンの時間なのかはレスポンスに加えないと使う人がどのタイムゾーン時間なのか分からず困るAPIになるので注意しないとな、と思いました。


DropboxAPIは全てUTCなのでタイムゾーンの情報は書いていませんね。ドキュメントにもUTC使うよ!と書いてくれているのでレスポンスにタイムゾーンの情報がなくても「ああ、なるほどね」となります。

https://www.dropbox.com/developers/documentation/http/documentation#file_requests-get


GitHubAPIUTCですね。

https://developer.github.com/v4/scalar/datetime/


Connpassは現地時間ですね。 ISO-8601フォーマットで2012-04-17T20:30:00+09:00のような形で返されます。

https://connpass.com/about/api/

Stapy50回記念で登壇しました

10/9(水)に行われた「みんなのPython勉強会」で登壇した振り返りをします。

startpython.connpass.com

「みんなのPython勉強会」とは、Python初学者からベテランまでいろんな層を対象にしてWebから科学までいろんな題材を扱う「みんな」のための勉強会です。

御縁があってその50回目の会の登壇枠にお誘いいただき、Pythonのunittest.mockモジュールについてお話させていただきました。

speakerdeck.com

ソースコードもまとめました。

https://github.com/mizzsugar/2019_stapy


いいことも改善点もありますが、ちゃんと振り返って精算して次の登壇に活かします!

(※以下、反省文)

登壇前準備

今回は御縁があって登壇のお誘いがあったので承諾してからがスタートです。普段からネタを貯めていた訳ではないのですがぼんやり話したいことはあったしせっかくなのでということで承諾しました。こんな手順で資料を作成しました。

トークの内容とトークの対象となる人を決める

前々からぼんやり「書籍や実践を重ねてテストに関する知見が溜まってきたので1年前の自分に伝えるつもりでいつか話したいな」と思っていました。特にunittest.mockモジュールの使い方に苦戦したので、これについて話そう!と決めました。ただ、話すだけだと焦点が分かりづらい発表になってしまうので「誰にどういう気づきを提供したいか」を詰めました。

結果、「テストコードの基本的な文法(assertEqualとかassertTrueとかレベル)はわかるけれども、unittest.mockモジュールの使いみちがよくわからない人」としました。「こういう時にこうunittest.mockモジュールを使うといい」かのヒントを得ることを目的としました。

トークの目次を作る

特に加筆することはないです。

③ 2で作ったトークの目次が対象の人にとって過不足がないか見直す

最初はunittest.mockモジュールをどういう時に使うかということだけ扱おうとしました。しかし、そもそも単体テストが何を目的として書くのかという話をしてからのほうが何をモックオブジェクトに置き換えるのか考えやすいなと思い、単体テストの例を最初に扱うことにしました。

サンプルコードを何にするかは悩んだので他の人に相談しました。最初に書いたのは軽減税率ではなかったのですが、「この話こそ軽減税率でしょ」という意見をもらい軽減税率にしました。軽減税率は詳しい人からのマサカリが怖かったので一番慎重に何回も書き直しました笑 一番自信がなかった軽減税率の部分は、いろんな人からお褒めいただいたので嬉しかったです。

④ 詳細を詰めながらスライド作り始める。

特に加筆することはないです。

⑤ 15分に収まるか練習

ひたすらストップウォッチと原稿ともに読む練習をしました。読んでいると直したい点が出てくるので読んでスライド手直して読んで・・・を繰り返しました。

また、人に聞いてもらって間違えた伝え方や誤解を生みやすい言い回しを修正しました。協力してくれた人には感謝しかありません。ありがとうございます。

登壇本番

ブラウザのトラブル

Googleスライドで原稿とストップウォッチを見ながら読む練習をしていたのですが、途中でブラウザの再読込をしないと次のスライドが映らないというトラブルに見舞われました。急いでスライドをspeakerdeckに変えたのですが、原稿とストップウォッチが見れないのと焦りの気持ちで15分オーバーしてしまいました・・・

原因は定かではないのですが、firefoxGoogle slideいじってておかしくなったかもしれない説が自分の中で有力です。Chrome試してみた時はなんともなかったので。次からはChromeで発表しようと思います。(※ちゃんと原因特定できていないのでもしかしたらfirefoxは何も悪くない可能性があります)

質疑応答

slidoという質疑応答アプリに匿名の質問が集まっていたので回答したのですが、匿名が故の難しさを感じました。というのも、2-3分の質疑応答で一方的な答えをちょちょっと言っただけでは質問者を納得させることが難しいような質問があったからです笑 

「モックとはなんですか」という極めて抽象的な質問をいただきました。抽象的な概念についての質問は質問者のレベル感を把握した上で、どこがどう疑問なのか対話を通して理解してもらうのが望ましいですが、それを考慮せず一方的に自分が思う答えをただ言ってしまったのが反省点です。

プログラミングを初めて1ヶ月の人が質問したとして、どう答えても理解は難しかったと思うので、「今回わからないのはどうあがいても仕方ないので、勉強を進めてテストコードをより良くしたい段階になった時にこの資料を見返して、その時にわからなかったらDMなりメールなりしてください」と答えるのが良かったかも、と今は思います。その場でわかってもらうことだけが正しいわけではないかもしれません。

初学者もいるコミュニティでの題材は初学者にも理解できる題材にすべきか

SNSやslido(質疑応答用アプリ)を見ると、「余計に混乱した」「結局モックがわからなかった」という声がありました。「発表を聞いてわからなくなった」という声がある場に居合わせたことがなかったので、「表立って言いたくなるほどわからなくなるようなだめな発表だったのかな」とショックを受けました。

15分の制約があったので、申し訳ないけれども、全くのプログラミング初学者はこのトークの対象から外す選択をしました。

やっぱ選択間違えたかなと思ったのですが、後々に

「誰もが楽しめる発表はないので対象を絞ったほうが良い内容になる」というアドバイスや「全く発表がわからなかったけれども発表を聞いてunitttest.mockを勉強したいと思いました」という感想をいただいたので、

内容はもっとブラッシュアップしつつ方向性はこの方針で行こうと決めました。

もしこのトークでもう1回登壇するならば

  • とはいえ「モック」の概念の説明が不足していたのでモックの概念をもう少ししっかり説明する

  • 本題に入る前に予めこの発表の対象となる人を伝えておく

  • 「モック」と「スタブ」の概念も扱って、どんな観点で単体テストを書くかをより深く扱う

  • 対話を通してでないと理解が難しいであろう質問は2-3分の質疑応答の時間で理解されなくても落ち込まない。交流会とかで詳しく話そうねというスタンスで挑む。

  • Chromeで登壇する

で行こうと思います。

登壇に誘って下さった方、聞いてくださった方、フォローしてくださった方、準備に協力してくれた方、ありがとうございました。

ざっくりPyConJP2019のSprint Dayの感想とDjangogirlsTutorial翻訳の感想

PyConJP本体の前にSprintに参加しました。

Sprintは、Pythonの何かしらの開発をする短期集中型イベントです。Sprint Dayに開発したいテーマを提案する人と、それをお手伝いする人で構成されています。

https://pycon.jp/2019/sprint

機械学習・Web・Core Python・ガジェットなど多岐にわたるテーマが揃っていました!

テーマ一覧↓

docs.google.com

きっちりしているものかと思いきや、どのチームも和気あいあいとしていて楽しかったです。

オープンな雰囲気でリラックスして開発に挑めました。

好きな時間に休めるし出入りも自由で、初参加の私はその自由さにいい意味で驚きました。

当日の様子↓


また、会場を貸してくださったHENNGEさんが

昼食はピザを夕食は豪華なケータリングを用意してくださって

豪華さに驚きました!! ありがとうございます!!!

このイベントが無料参加とは思えない・・・


こんな素敵なイベントで、私はDjangogirls Tutorialの翻訳のお手伝いをしました。

Djangogirls Tutorial翻訳をやってみて

翻訳は理解を深めるためにたまにやるのですが、査読付きのちゃんとしたものは大学受験ぶりでした笑

(最後に技術関連の翻訳をちゃんとアウトプットしたのいつだっけ?と思ったら今年の2月でした↓)

【非公式翻訳】ForeignKey in Django公式ドキュメント - mizzsugar’s blog


ちゃんとできるか心配でしたが、Djangogirls Tutorialの英語が比較的易しかったのと

翻訳ツールがとても優れているおかげでなんとかなりました。

「これをどうやって訳すか悩んでて」と気楽に相談できる雰囲気もありがたかったです。

なんと、このSprint DayでDjangogirls Tutorialの翻訳率は100%に到達しました!!

これから翻訳のレビューがあるので完全に、というわけではありませんがこれは嬉しかったです。貢献できてよかったです。


Djangogirls Tutorialの翻訳は、普段ドキュメントを読んだり書いたりするのとは違う観点での伝え方が必要だと感じました。

普段の開発中でのコミュニケーションでは「雰囲気でざっくりこんな感じ」というのは好ましく思われておらず

いかに正確に伝えるかに焦点を当てています。また、基本的に会話なのである概念についてわからなかったら理解できるように粘り強く対話します。

なので、「厳密にはもうちょっと言いたいけど初心者が今の段階ではこの概念だけわかればOK」という範囲を絞るのと(まあ、これは普段のDjangogirlsのイベントでもやってるけど)

Tutorialというある意味一方的?な媒体でどう伝えるのかを考えるのは新鮮で面白い体験でした。


Sprint Dayでいろんな人とお話できたし、微力ながらもPythonの世界に貢献できたし、とても満足です!

来年はどんなテーマのものに参加するか、もしくは自分がテーマを掲げるかわかりませんが

また参加したいと思います。

ざっくりPyConJP2019の2日目感想

ざっくり2日目の感想を書き残します。


Pythonで始めてみよう関数型プログラミング

Twitter

twitter.com

スライド

Pythonで始めてみよう関数型プログラミング - Speaker Deck

YouTube

www.youtube.com

安全でわかりやすいコードを書くためのプログラミングの考え方としてとても面白かったです。

mypyのFinalを使って変数がImmutableであると宣言したり、Dictを型クラスのような役割に定義したクラスのインスタンスにしたりは普段やっているのですが、

それらを便利に実装できるようなライブラリがあるというのは勉強になりました。

また、F#から刺激をうけている様子を拝見して、久々に他言語の人とPython以外の言語でプログラミングしたくなりました。


Python Website is Slow? Think Again!

Twitter

twitter.com

スライド

https://freedomofkeima.com/pyconjp2019.pdf

YouTube

www.youtube.com

PythonのWebアプリケーションが遅いと言われているのはI/OバウンドがボトルネックになっていてPython自体が悪いんじゃないのではないか?というお話でした。

DBの呼び出しを何回もしないようにPythonのデコレーターを使って余分にDB呼び出ししていたのを抑えたり

AsyncIOを使うという解決策を紹介してくれました。

また、CPUに高負荷かける処理するようなあらCとかGo使ったほうがいいかもとのことでした。

入門自作検索エンジン

Twitter

twitter.com

スライド

The first step self made full text search - Speaker Deck

YouTube

www.youtube.com

検索のためのアルゴリズムの説明から説明してくださり、勉強になりました。

Pythonで簡潔なコードを書いてくださったのもありがたかったです。1回の講演では理解しきれなかったので改めて資料を読み直したいと思います。

PyConJPのAPIを使うというのも面白いかったです。


当日飛び込みLTに当選した話

去年参加したときはまさか次の年に自分があの段に立っているとは思いませんでした。

PyConJP2018で_勇気をもらって_Pythonエンジニアになった話.pdf - Speaker Deck

こぼれ話ですが、去年のPyCon後に転職活動して内定を獲得後、

たまたま参加した勉強会にこのLTのきっかけとなった方がいまして。

「あのLTのおかげで勇気もらってPythonエンジニアになれることになりました!」と報告したら「来年は同じ内容でぜひあの場所で登壇してください」と言ってもらいました。(ご本人覚えていないかもしれませんが・・・)

無事果たせてよかったです。

ざっくりPyConJP2019の1日目感想

今年もPyConJPに参加しました!

1日目に拝聴したセッションの感想をざっくり書き残そうと思います。

なお、今日の発表の様子はYouTubeにすべて上がっています。

t.co

個人的に、YouTube版はちゃんと聞き取れるしスライドの内容もちゃんと見えるので良かったです!


Djangoで実践ドメイン駆動設計による実装

Twitter

twitter.com

YouTube

youtu.be

めちゃめちゃ大きいテーマに挑戦されていて、尊敬します。

DDDの考え方もしっかり説明してくださってありがたかったです。Pythonで具体的なコードまで載せており、こういう実装方法もあるんだと学びになりました。

「あれ? 結局これ取り入れてで何を解決したいんだっけ?」とならないように

戦略的DDDをしっかり行うという前提でDjangoでの戦術的DDDに挑戦したいなあと身が引き締まりました。


Pythonを使った APIサーバー開発を始める際に 整備したCIとテスト機構

Twitter

twitter.com

スライド

speakerdeck.com

「CIに教わる」という名言が印象的でした。

モックにどれだけたよっているか、どんなふうにたよっているかは

きれいにDIできていて修正時の影響範囲を抑えたコードになっているかを確かめる観点ということで

改めて自分のテストコードを見て確認したいと思いました。

Pythonのモック、なんでもモックできちゃうので

「ただモックした!」から前進するヒントになりそうな発表でした。

YouTube

youtu.be


ListはIteratorですか?

Twitter

twitter.com

スライド

docs.google.com

YouTube

youtu.be


とても楽しそうにお話していたのが印象的でした。

スライドにあるListやSetやIterableなどの各クラスの継承関係の図はありそうで今までないやつだったので感動しました・・・! あれは重宝しそう・・・!

あの図を見ながらなら、広く引数の型を受け入れる使いやすい関数を書けそうな気がします。

初学者にもわかりやすくも、継承関係や豆知識で深いところまで扱っていて面白かったです。


[おまけ]セッション以外で感じたこと

去年の初参加のときはPython始めたばかりで全然知り合いがいなかったのですが、

今年は同じ会社の人がいたり勉強会で一緒だったorTwitterでやりとりしていた方など

いろんな人とお話できて楽しかったです!

知り合いがまったくいなかった去年もトークを聞くだけでも十分楽しかったですが、

いろんな人がいっしょくたに集まるのは感慨深いですね。

アフターパーティで私の友達が私の会社の人と友達になる(友達の友達は友達的なやつ?)というのもこういうイベントならではなのかな〜など月並にも思いました。