Django: StreamingHttpResponseをHttpResponseと比較してみたまとめ
使い分けについての結論
(最後の考察で詳しくまとめています。)
ファイルサイズが小さい様なら、HttpResponseを使う。
Djangoで巨大なファイルを作成する際には、StreamingHttpResponseを使わざるをえない。しかし可能な限り避けた方がよく、CSV出力はDjangoのrequest-responseの外で別の方法によってやるべき。
前書き
StreamingHttpResponseクラスは、レスポンスのデータを分割して少しずつ送るためのクラスです。 公式ドキュメントでは、サイズの大きなCSVファイルをダウンロードさせるケースを例として上げています。
Outputting CSV with Django | Django documentation | Django
上記公式ドキュメントを参考に、CSVファイルを作成する上での通常のHttpResponseクラスとStreamingHttpResponseクラスの挙動を比較しました。
CSVを作成するコードを比較
上記公式ドキュメントを元に、HttpResponseクラスとStreamingHttpResponseそれぞれを利用した場合のコードを以下に示します。
HttpResponseを利用してCSVデータを作成するコード例
通常のHttpResponseと異なり、CSVファイルを作成する際には、content_typeを「text/csv」とします。
これによって、レスポンスヘッダにContent-Type: text/csv
がセットされます。(通常はtext/html
)
また、HttpResponseオブジェクトはfile-likeなオブジェクトです。writeメソッドを持っています。 そのため、直接csv.writer()
の第一引数にHttpResponseオブジェクトを指定し、witerow系のメソッドで書き込みを行うことができます。
def create_csv(request): data = [[f'Row {i}', str(i)] for i in range(5_000_000)] response = HttpResponse(content_type='text/csv') response['Content-Disposition'] = 'attachment; filename="t.csv"' # HttpResponseは "file-like object" であり、writeメソッドを持つ。 # (ref.) csvfile can be any object with a write() method. writer = csv.writer(response) writer.writerow(['name', 'Number']) writer.writerows(data) return response
StreamingHttpResponseを利用してCSVデータを作成するコード例
HttpResponseオブジェクトとStreamingHttpResponseオブジェクトは、名前こそ似ていますが挙動は全く異なります。StreamingHttpResponseクラスの第一引数は、generatorです。
あとで詳しく述べますが、とにかく第一引数にはイテレーション可能なオブジェクトを指定する必要があります。(listでもいいのですが、generatorでないとメモリに巨大な配列を読むことになるので、StreamingHttpResponseを使う意味が薄れてしまうと思います)
class Echo: """streamのbufferに入れるのではなく, returnするwriteメソッドを定義""" def write(self, value): return value def create_large_csv(request): rows = ([f"Row {i}", str(i)] for i in range(5_000_000)) pseudo_buffer = Echo() writer = csv.writer(pseudo_buffer) # nextするたびにcsvファイルの1行分を取得するgenerator file_generator = (writer.writerow(row) for row in rows) response = StreamingHttpResponse(file_generator, content_type="text/csv") response['Content-Disposition'] = 'attachment; filename="t.csv"' return response
ダウンロードの挙動とかかった時間を比較
上記のコードでは、5_000_000行のCSVデータを作成&ダウンロードします。
HttpResponseを利用してCSVデータをダウンロードした場合
- ダウンロードを開始するまで、しばらく時間がかかります。
- 一度ダウンロードを開始すると、比較的速やかにダウンロードが完了します。
- 今回の例だと、全体で50秒程度でした。
StreamingHttpResponseを利用してCSVデータをダウンロードした場合
- ダウンロードはすぐに開始します。
- ダウンロードが完了するまでには、HttpResponseと比べて約2倍の時間がかかりました。
- 今回の例だと、全体で100秒程度でした。
上記から、少なくともHttpResponseで返すことができるサイズのCSVファイルならば、HttpResponseで返した方が良さそうです。 ただしCSVファイルのサイズがあまりに大きい場合には、メモリへの読み込みやタイムアウトを防ぐためにStreamingHttpResponseを使った方が良さそうです。今回は単純なテキストですが、DBからデータを読み込む場合にはまた変わってくると思います。
仕組みを比較
HttpResponseとStreamingHttpResponseの挙動の違いはどこにあるか、ソースコードを比較してみます。 以下の記事を参考にさせていただきました。
How Does Django’s StreamingHttpResponse Work, Exactly? – Andrew Brookins
HttpResponse
コードは適宜,省略しています。
HttpResponseでは、コンストラクタに渡したデータをcontentプロパティによってjoinし、一つのbytesにしてしまいます。 そのため、イテレーションによって一回だけ取り出されるbytesがcontentとなるようです。
つまり、HttpResponseも引数としてイテレーション可能なオブジェクトをとることができます。公式ドキュメントにもその挙動について、言及されています。
Request and response objects | Django documentation | Django
class HttpResponse(HttpResponseBase): streaming = False def __init__(self, content=b'', *args, **kwargs): super().__init__(*args, **kwargs) # Content is a bytestring. See the `content` property methods. def __iter__(self): return iter(self._container) @property def content(self): return b''.join(self._container)
StreamingHttpResponse
コードは適宜,省略しています。
StreamingHttpResponseでは、渡されたイテレーション可能なオブジェクトの各要素に対してmake_bytes
関数を適応したものをイテレーションしていく仕組みになっています。
class StreamingHttpResponse(HttpResponseBase): streaming = True def __init__(self, streaming_content=(), *args, **kwargs): super().__init__(*args, **kwargs) # `streaming_content` should be an iterable of bytestrings. # See the `streaming_content` property methods. self.streaming_content = streaming_content @property def streaming_content(self): return map(self.make_bytes, self._iterator) def __iter__(self): return self.streaming_content
StreamingHttpResponseの使い方の補足
公式ドキュメントでは、writeするとその値をそのまま返すEchoというクラスを定義してジェネレーターを作成していますが、(個人的には)若干わかりづらいのでStreamingHttpResponseの挙動を考えるために以下の様な関数get_csvlike_oneline
を作成し、前述のコードを書き直してみました。
def get_csvlike_oneline(value: list): csv_oneline = ','.join(value) return csv_oneline + '\r\n'
def create_large_csv(request): rows = ([f"Row {i}", str(i)] for i in range(5_000_000)) # 新しいコード file_generator = (get_csvlike_oneline(row) for row in rows) response = StreamingHttpResponse(file_generator, content_type="text/csv") response['Content-Disposition'] = 'attachment' return response
結局やっていることは、nextするたびにcsvの「1行」として意味のあるコードを返すgeneratorを定義しているだけです。 Echoは、csvモジュールの機能(自動でカンマを入れたり、改行記号を入れる)を利用するため、Echoによって加工した結果を得ているだけです。
もちろん、csvの「1行」でなくてもOKです。他の形式(text)でも、streamingはできます。必要なのは、1つのチャンクとして有効なデータがnextで取れるということだけです。
考察: StreamingHttpResponseの使い所
上述のとおり、StreamingHttpResponseは巨大なファイルを出力する際には必要となってきます。しかし公式ドキュメントに、以下の様に注意書きがありました。
Request and response objects | Django documentation | Django
Performance considerations
Django is designed for short-lived requests. Streaming responses will tie a worker process for the entire duration of the response. This may result in poor performance. Generally speaking, you should perform expensive tasks outside of the request-response cycle, rather than resorting to a streamed response.
つまり、結局streamingを使ってファイルを出力する様にしてもworker processを長時間束縛してしまうことになってしまうので、パフォーマンスが落ちてしまう様です。
つまり、そもそも巨大なCSVなどのファイルを作成するにはDjango自体が向いていないのでやらない方が良さそう..という風に解釈しましたが、じゃあどこでやればいいのか書いてないのでよくわかりませんでした。
lambdaとかで作ればいいんでしょうか。outside of the request-response cycle
ということは、いったんレスポンスは適当に返して、ファイルをバックエンドで作成してどこかにアップロード->それをダウンロードしてもらう..ようなフローを辿ればいいのでしょうか..(ワカラナイ...)
この辺は機会があれば調べてみたいと思います。
サーバーサイド処理しているDataTablesをPDFで出力する
前書き
DtataTablesで表示するデータをサーバーサイドで用意し、またPDFを出力する機能をDjangoで作成することになりました。その際、 thinkAmiさんから助言をいただきました。本記事は、Datatables/django-datatables-view/WeasyPrint/Djangoを使用し、PDF出力を実装する方法 をまとめたものです。
また、使用しているコードの全体は以下のGitHubレポジトリにアップしました。
環境,ツールのバージョン
MacOS Catalina 10.15.6
バージョン | |
---|---|
Python3 | 3.8.2 |
Django | 3.0.7 |
WeasyPrint | 15 |
django-datatables-view | 1.19.1 |
使用するツール
- DataTables
- テーブル要素に検索やソート、ページネーション機能を持たせるjQueryライブラリ
- django-datatables-view
- 上記のDataTablesをサーバーサイド処理する際に、Djangoでバックエンドを簡単に作成するためのViewを使うことができる。
- WeasyPrint
- HTMLテンプレートを用いてPDFを作成するためのPythonツール。
仕様と実装方針
以下の様な、シンプルな名簿を想定します。
表
参加者 | 出身 | 年齢 |
---|---|---|
太郎 | 東京 | 10 |
二郎 | 大阪 | 15 |
三郎 | 北海道 | 22 |
こちらをテーブルとして表示し、ソートや検索機能を実装します。 ただし、名簿は数万件規模となるためページネーション可能とし、処理速度を確保するためデータは表示するもののみサーバーサイドで提供することにします。
またPDFとして出力する機能を実装します。 PDFはデータテーブルで表示している内容を、ページネーション含め全件出力します。ただし、テーブルをソートしたり検索して絞り込んだ結果を反映可能にします。
プロジェクトの作成とインストール
仮想環境をvenvで作成し、必要なライブラリをpipでインストールします。
# 仮想環境構築 $ mkdir project $ cd project $ python3 -m venv env $ source env/bin/activate # ライブラリのインストール $ pip install Django $ pip install django-datatables-view
WeasyPrintは依存ライブラリがあるので、homebrewでインストールしました。 詳しくは公式ドキュメントInstalling — WeasyPrint 51 documentationをご覧ください。 Amazon Linuxでの環境構築は、手前味噌ですがWeasyPrintでPDF出力するまでの環境構築(Django/Amazon Linux)をご覧ください。
$ pip install WeasyPrint $ brew install cairo pango gdk-pixbuf libffi
Djnagoのプロジェクトを作成します。cmsというappを作成します。 詳しくはGitHubにあげたこちらのリンクを参考にしてください。
DataTablesでの表示(Server-side processing)
DataTablesは通常、HTMLに表示済みのテーブルに対してページネーションや検索の機能を付与します。つまり実際には、ページネーションで表示されていない部分はdisplayをnoneとしただけで、レンダリングされたままです。
しかしデータ数が非常に多い場合には、大量のデータをHTML上に表示しているため動作が遅くなり、最悪の場合はサーバー側でタイムアウトしてしまいます。 そのため、1ページ分のデータのみをサーバー側から返す様にします(Server-side Processing)。
- DataTablesの公式ドキュメントにもPHPでバックエンドを作成する例が示されています。
- DataTables example - Server-side processing
DataTablesサーバーサイド処理の実装
cms/templates/cms/index.html
- 省略。こちらのGitHubのリンクを参照してください。
- こちらの表示は、
cms/urls.py
にてTemplateViewを用いて行っています。
- こちらの表示は、
cms/static/js/index.js
- DataTableを表示するためのjsコードです。該当部分のみ示します。
- "serverSide"をtrueとし、"ajax"でDjangoによって作っているURLを指定します。URLはGitHubのリンクを参照してください。
$('#table_id').DataTable({ "processing": true, "serverSide": true, "ajax": { 'url': '/datatable-view/', 'type': 'GET' } });
cms/views.py
- DataTablesにデータを返すバックエンド側です。
- DataTablesはソートなどの情報を複雑なクエリストリングを渡してきますが、django-datatables-viewを使えば適切に解析してくれます。
from django_datatables_view.base_datatable_view import BaseDatatableView from cms.models import Person class IndexDatatableView(BaseDatatableView): model = Person # この順番で表示される。 columns = ['name', 'prefecture__name', 'age'] def render_column(self, row, col): if col == 'prefecture__name': return row.prefecture.name return super().render_column(row, col)
- 補足: render_columnのオーバーライド
- django-datatables-view · PyPI
- 県の名称(prefectureテーブルのname属性)は外部キーの値です。そのため、render_columnメソッドをオーバーライドして、表示したい値を返す必要があります。
- モデルの構造については、仕様と実装方針をご確認ください。
PDF出力
余談: DataTablesのPDF出力機能
DtataTablesにもPDF出力機能はあります。もしサーバーサイドでデータを作らず、全件HTMLに表示する様な使い方をしているなら、こちらを使用します。
DataTables example - PDF - open in new window
サーバーサイド: WeasyPrintを使ったPDF出力
サーバーサイドでPDFを作る方法を以下に示します。 PDFを作成するPythonライブラリとしてはReportLabが有名な様です。Djangoの公式ドキュメントでは、こちらを利用した例が乗っています。
Open Source - ReportLab.com Outputting PDFs with Django | Django documentation | Django
ただしReportLabは表示のためのコードが複雑です。今回はWeasyPrintを用います。こちらはCSSを当てることで、スタイルを簡単に適応することができます。
今回の方針としてDataTablesでソートや絞り込みを行ったデータを、全ページネーション分PDFとして出力することにします。 そのため、django-datatables-viewのクラスを利用することにします。
イメージとしては、DataTables表示のためのソートや検索の処理後、DataTables表示用JSONレスポンスを作る代わりに、PDF出力用レスポンスを返す様に します。
フロント: PDF印刷のためのボタンとリクエストを作る
URLはDataTablesのサーバーサイド処理と共通で使います。Viewも共通です。そのため、「PDFの出力リクエストである」ことを示すための目標を付加する必要があります。
今回は、クエリストリングにoutput=pdf
を付加します。output=pdf
がクエリストリングに付加されている時に、PDF印刷処理に分岐する様にします。
cms/static/js/index.js
- 補足です。
- DataTableオブジェクトの
table.ajax.params
メソッドによって、画面上での操作によるソートや検索の条件をオブジェクトとして取得することができます。 $.param
メソッドによって、オブジェクトをクエリストリングにすることができます。- 最後に、前述した
output=pdf
を付加しています。ただの目印なので、わかればなんでもOKですが、datatablesが利用しているKeyは避けます。
- DataTableオブジェクトの
$('#pdf_button').on('click', function () { const table = $('#table_id').DataTable(); const query_params = table.ajax.params(); // 1ページあたりのデータ取得件数: -1は全件。 query_params.length = -1; query_string = $.param(query_params); const url = '/datatable-view/' + '?' + query_string + '&' + 'output=pdf'; window.open(url, '_blank'); });
バックエンド: リクエストを元にPDFを組み立てる
cms/views.py
- 先ほどの、DataTablesのサーバーサイドのviewに記述を付加します。
prepare_results
関数はBaseDatatableViewのメソッドです。ソートやフィルター済みの、Djangoのクエリセットオブジェクトを引数として受け取ります。この関数に割り込み、クエリセットをself.query_set_for_pdf
と命名したインスタンス変数に保持します。後に、これを用いてHTMLをレンダリングし、それをもとにPDFを作成します。render_to_response
関数は、BaseDatatableViewがレスポンスを返す本当に最後の部分の関数です。この部分でクエリストリングのoutput=pdf
を判定し、元の処理(DataTabelsに描画用のjsonを作る)のか、PDFを出力するのかを分岐する様にしています。- なお、
self._querydict
はBaseDatatableViewが処理の上流で作成している、queryパラメータの一覧です。
- なお、
その他、CSSは以下を参考に当てておきます。
cms/static/css/pdf.css
を作成しました。
class IndexDatatableView(BaseDatatableView): model = Person # この順番で表示される。 columns = ['name', 'prefecture__name', 'age'] def render_column(self, row, col): if col == 'prefecture__name': return row.prefecture.name return super().render_column(row, col) def prepare_results(self, qs): """PDF出力様に、ソートやフィルター後のquerysetを保持""" self.query_set_for_pdf = qs return super().prepare_results(qs) def render_to_response(self, context): """ output=pdfのクエリパラメータが存在するときPDFを出力""" if self._querydict.get('output', 0) == 'pdf': return self.create_pdf() return super().render_to_response(context) def get_dataset_for_pdf(self): return { 'dataset': [ [person.name, person.prefecture.name, person.age] for person in self.query_set_for_pdf ] } def create_pdf(self): context = self.get_dataset_for_pdf() html_string = render_to_string( template_name='cms/pdf_template.html', context=context, request=self.request) pdf_file = HTML(string=html_string).write_pdf( # ここでCSSを指定可能.projectのrootからのパスを書く。 stylesheets=[CSS('cms/static/css/pdf.css')]) stream = io.BytesIO(pdf_file) return FileResponse( stream, as_attachment=True, filename='test.pdf')
cms/templates/cms/pdf_template.html
- PDF出力のためのtemplateです。データを埋める部分のみ表示しています。全体は、GitHubをご参照ください。
- Djangoのテンプレートシステムを使って動的にデータをテーブル要素に埋めています。
<tbody> {% for row in dataset %} <tr> {% for cell in row %} <td>{{ cell }}</td> {% endfor %} </tr> {% endfor %} </tbody>
動作確認
localhost:8000/idnex/
にアクセスし、DataTablesを操作した後にPDFボタンを押します。以下の様なPDFが出力されたら成功です。
WeasyPrintでPDF出力するまでの環境構築(Django/Amazon Linux)
- 前書き
- 参考にさせていただいたリンク
- Amazon Linuxの立ち上げ
- 必要なライブラリのインストール
- 必要なコードの作成とPDF出力
- 文字化けへの対応
- フォントの変更
- 補足: FileResponse
前書き
サーバーサイドからPDFを出力する機能をDjangoで作成することになりました。その際、 thinkAmiさんからWeasyPrintを教えていただきました。本記事は、WeasyPrintを使ってDjango/Amazon Linux環境から、PDFを出力する際に必要な設定をまとめたものです。
参考にさせていただいたリンク
Installing — WeasyPrint 51 documentation
Django2.0 + WeasyPrint でお手軽にPDF出力 - Qiita
Amazon Linuxの立ち上げ
必要なライブラリのインストール
前準備
$ sudo yum update -y $ sudo yum install gcc $ sudo yum install python3
WeasyPrintを動かすための準備
$ python3 -m venv env $ source env/bin/activate # WeasyPrint自体はpipでインストールできます。 $ pip install WeasyPrint # 依存ライブラリをインストールします。 $ sudo yum install cairo $ sudo yum install pango $ sudo yum install gdk-pixbuf2 # 以下が動けば成功です!(WARNINGは気にしなくてOKです。適応できなかったCSS由来がほとんどです。) $ weasyprint http://weasyprint.org ./weasyprint-website.pdf
- Django のプロジェクトを立ち上げます。
# sqliteを入れるのに必要です。 $ sudo yum install expect # sqliteを入れます。 $ wget https://www.sqlite.org/src/tarball/sqlite.tar.gz?r=release $ tar xzf sqlite.tar.gz\?r\=release $ cd sqlite $ ./configure --prefix=/usr/local $ make $ sudo make install $ export LD_LIBRARY_PATH="/usr/local/lib" # Djangoのproject立ち上げます。 $ pip install Django $ django-admin startproject config . $ python manage.py startapp cms $ python manage.py migrate
必要なコードの作成とPDF出力
PDFを出力するViewとTemplates
cms/views.py
- 基本的に、HTMLのstringをテンプレートエンジンを用いて作成し、HTMLクラスのstring引数に渡してインスタンス化し、write_pdfメソッドを呼ぶだけです。
- 詳しくは、最後の項目「補足」を参照してください。
import io from django.template.loader import render_to_string from weasyprint import HTML from django.http import FileResponse def pdf_export(request): html_string = render_to_string( "cms/weasy_print_test_template.html", context={'name': 'SIA', 'value': 'PITA'}, request=request) writer = HTML(string=html_string) pdf_file = writer.write_pdf() stream = io.BytesIO(pdf_file) response = FileResponse(stream) return response
config/settings.py
- installed appにcmsを追加しないと、templatesを探索できないので注意。
cms/templates/weasy_print_test_template.html
INSTALLED_APPS = [ 'cms.apps.CmsConfig', # ..... ]
<h1>テスト用のテンプレート</h1> <p>It's a test template. -- {{ name }} : {{ value }}</p> <h2>Request Object</h2> <ol> <li>request.method {{ request.method }}</li> <li>request.path {{ request.path }}</li> </ol> <h2>User Object</h2> <ol> <li>user.is_authenticated {{ user.is_authenticated }}</li> <li>user.is_anonymous {{ user.is_anonymous }}</li> </ol>
- urlのルーティング
config/urls.py
from django.contrib import admin from django.urls import path, include urlpatterns = [ path('admin/', admin.site.urls), path('', include('cms.urls')), ]
cms/urls.py
from django.urls import path from cms.views import pdf_export app_name = 'cms' urlpatterns = [ path('pdf/', pdf_export), ]
確認
- EC2のinbound ruleに、自分のIPアドレスをポート8000で登録しておきます。
- あまり良くないですが、テストということで以下を実行してアクセスします。
$ python manage.py runserver 0:8000
アクセス 自分のEC2へのURL:8000/pdf/
- 以下の様なものが出力されるはずです。
文字化けしています。これを直しましょう。
文字化けへの対応
$ fc-list /usr/share/fonts/dejavu/DejaVuSansCondensed-Oblique.ttf: DejaVu Sans,DejaVu Sans Condensed:style= Condensed Oblique,Oblique /usr/share/fonts/dejavu/DejaVuSansCondensed-Bold.ttf: DejaVu Sans,DejaVu Sans Condensed:style=Con densed Bold,Bold ....(略)
日本語フォントのインストール
- 今回はこれを使わせていただきます。
- M+ FONTS | JAPANESE
上記のフォントを配布してくださっているページに以下の様に記述があるので、それに従ってインストールします。
公式配布サイト https://osdn.net/rel/mplus-fonts/TESTFLIGHT より、最新版の mplus-TESTFLIGHT-*.tar.xz ファイルをお選びください。ファイルの展開は Unarchiver (macOS)、7-Zip (Windows)などのフリーソフトウエアが対応しています。
フォントのインストールは、/usr/share/fonts
にダウンロードして解凍したフォントをおくだけです。lsしてみると、先ほどfc-list
で確認したフォントがまとまっているのがわかります。fonts直下にdefavu
ディレクトリがあり、その中にttfファイルが入っている感じです。
$ cd /usr/share/fonts $ sudo wget https://osdn.net/dl/mplus-fonts/mplus-TESTFLIGHT-063a.tar.xz $ sudo xz -dc mplus-TESTFLIGHT-063a.tar.xz | sudo tar xfv - $ sudo rm -rf mplus-TESTFLIGHT-063a.tar.xz
もういちど、runserverしてアクセスします。
いいかんじですね。
フォントの変更
せっかくなのでフォントを変えてみます。 先ほど日本語フォントを1種類だけ入れたので、勝手にそれが当てられているようですが、WeasyPrintはCSSとしてフォントを指定することができます。
cms/views.py
- 基本的に、HTMLのstringをテンプレートエンジンを用いて作成し、HTMLクラスのstring引数に渡してインスタンス化し、write_pdfメソッドを呼ぶだけです。
- Release sawarabi-gothic 20161015 - Sawarabi Fonts - OSDN
- 使用したフォントはこちら。
import io from django.template.loader import render_to_string from weasyprint import HTML, CSS from django.http import FileResponse def pdf_export(request): html_string = render_to_string( "cms/weasy_print_test_template.html", context={'name': 'SIA', 'value': 'PITA'}, request=request) writer = HTML(string=html_string) pdf_file = writer.write_pdf( # Sawarabi Gothicを指定。 stylesheets=[CSS(string='body { font-family: Sawarabi Gothic;}')] ) stream = io.BytesIO(pdf_file) response = FileResponse(stream) return response
フォントの変更ができました。
補足: FileResponse
この部分は、通常のHttpResponseで書くこともできます。
response = HttpResponse(
content=pdf_file,
content_type='application/pdf')
FileResponseを使った場合の利点は
- ファイルサイズが巨大な時、分割してダウンロードすることでタイムアウトを防ぐことができます。
- ダウンロードするファイル名や方法の指定がスマートです。
1については下記のドキュメントをご参照ください。 2については、HttpResponseと比べると以下の様になっています。
FileResponseの場合
# PDFファイルをブラウザに表示する(firefox) response = FileResponse(stream, as_attachment=False, filename='t.pdf') # PDFファイルをダウンロードし、開くか保存するかを選択するダイアログを表示する(firefox) response = FileResponse(stream, as_attachment=True, filename='t.pdf')
HttpResponseの場合
response = HttpResponse( content=pdf_file, content_type='application/pdf') # PDFファイルをブラウザに表示する(firefox) response['Content-Disposition'] = 'inline; filename="test.pdf"' # PDFファイルをダウンロードし、開くか保存するかを選択するダイアログを表示する(firefox) response['Content-Disposition'] = 'attachment; filename="test.pdf"'
HttpResponseでは、PDFファイルのブラウザによるContetn-Dispositionの振る舞いについてヘッダを直接セットする必要がありますが、FileResponseは引数によってより自然に指定可能です。 PDFファイルのサイズが大きくなる場合も想定されるので、FileResponseを使った方が良いのではないかと思います。
- FileResponseの使い方については、公式ドキュメントの以下の部分が参考になります。
Request and response objects | Django documentation | Django
Outputting PDFs with Django | Django documentation | Django
- 大きなファイルのダウンロードについては、公式ドキュメントの以下の部分が参考になります。
pandasのDataFrameとDynamoDBの相互変換
はじめに
無理矢理感があるので、あくまでこういう方法でもできる、という程度の個人的な覚書です。
- DataFrameで持っているデータをそのままDynamoDBに突っ込む
- DynamoDBのデータをDataFrameに入れる
この二つをboto3を使って行います。
前提として
- パーティションキー: name
- ソートキー : year
- そのほかのattribute: title, info
DataFrameで持っているデータをDynamoDBにput_itemする
以下の様なDataFrameがあるとします。ポイントとして
- NaNが入っている
- 各レコードをdictとし、そのリストの形にしたい
In [175]: df Out[175]: name title year info 0 レミリア 東方紅魔郷 2002 {'spell_card': ['紅色の幻想郷']} 1 博麗霊夢 NaN 2002 NaN 2 博麗霊夢 東方妖々夢 2003 NaN 3 博麗霊夢 東方永夜抄 2004 NaN
# tableの準備 import boto3 dynamodb = boto3.resource('dynamodb', region_name='ap-northeast-1') table = dynamodb.Table('Character') import numpy as np import pandas as pd # orient='records' がポイントです。list of dictになります。 data = df.to_dict(orient='records') with table.batch_writer() as batch: for item in data: # ここで、NaNが入っているkeyを落とします。nanのままだとエラーになります。 item_not_has_nan = {key: item[key] for key in item if item[key] is not np.nan} batch.put_item(Item=item_not_has_nan)
DynamoDBのデータをDataFrameに入れる
そのまま入れられます。
ret = table.scan() df = pd.DataFrame(data=ret['Items']) df name title year info 0 レミリア 東方紅魔郷 2002 {'spell_card': ['紅色の幻想郷']} 1 博麗霊夢 NaN 2002 NaN 2 博麗霊夢 東方妖々夢 2003 NaN 3 博麗霊夢 東方永夜抄 2004 NaN
おまけ: 取得するattributeを指定する
DataFrameにした後でも加工は可能ですが、取得したいAttributeが決まっているなら基本的にはDynamoDB側で落とす方がいいと思います。
以下ではname, titleのみ取得しています。ただし、nameはdynamoDBの予約語です。ですので、一度変数(#n
)を指定してから、別の引数でそれを置換するような書き方(下記)が必要です。
ret = table.scan(ProjectionExpression="#n, title", ExpressionAttributeNames={'#n': 'name'}) pd.DataFrame(data=ret['Items']) name title 0 レミリア 東方紅魔郷 1 博麗霊夢 NaN 2 博麗霊夢 東方妖々夢 3 博麗霊夢 東方永夜抄 4 フランドール 東方紅魔郷
ローカルでDynamoDBを動かす
はじめに
boto3やaws cliからDynamoDBを扱う練習をするのに、公式の以下のツールが簡単で便利なので、個人用として使い始めました。そのセットアップ方法のまとめです。
DynamoDB ローカル
- コンテナとして動かして動かすことができます。本物のDynamoDBと同じ様にcliやboto3から操作できます。
$ docker pull amazon/dynamodb-local $ docker run -p 8000:8000 amazon/dynamodb-local
credential(.aws/credentails)について
- defaultが使われるが、てきとーな名前でOK.
- configureコマンドから、「TESTLOCAL」とでも入れておけば使えます。
設定の変更方法
オプション一覧です。
こちらを参考にさせていただきました。
デフォルトだとメモリに保持するので、データが残りません。以下の様に立ち上げます。
2020/08/30 追記: --rm
オプションを追加しました。コンテナを落とす際に、破棄しています。
$ ls local_path # docker側の/dynamodb_data_pathをこちらにマウントする。 なお、dbPathによってdocker中のファイル位置を決めとく必要ある。 $ docker run --rm -p 8000:8000 -v $PWD/local_path:/dynamodb_data_path amazon/dynamodb-local -jar DynamoDBLocal.jar -dbPath /dynamodb_data_path -sharedDb
実例
$ aws dynamodb create-table \ > --table-name Music \ > --attribute-definitions \ > AttributeName=Artist,AttributeType=S \ > AttributeName=SongTitle,AttributeType=S \ > --key-schema AttributeName=Artist,KeyType=HASH AttributeName=SongTitle,KeyType=RANGE \ > --provisioned-throughput ReadCapacityUnits=1,WriteCapacityUnits=1 $ aws dynamodb list-tables --endpoint-url http://localhost:8000
- boto3から。
- 「default」のcredentialsを使っているので、この記述で↑で作ったテーブルが参照できる。
dynamodb = boto3.resource('dynamodb', endpoint_url='http://localhost:8000') for i in dynamodb.tables.all(): print(i.table_name) Music
NoSQL Workbench
ローカルのDynamoDBだとGUIがないので、公式のこちらのツールをつかって表示できます。 本物のDynamoDBもこれを使った方がいい感じに表示したりできます。
以下の様な感じで、operation builder -> add connection -> DynamoDBローカルのタブを選ぶ...という感じで選択できます。名前は適当でOKです。
注意として、credentialが勝手に生成されるんですよねこれ.... この時できたアクセスキーとシークレットキー(ローカルでのみ有効な適当な文字列)を設定ファイル(.aws/credential)に書いておきます。
割と見やすいです。他にもテンプレートからモデル作ったりとか色々できるらしいですが、現状、ただのデータを表示する箱としてしか使えてません....
DRFで例外が発生してからレスポンスを返すまでの処理をカスタムする
DRFには、例外(Exception)をResponseの形で返す仕組みが備わっています。その個人的なノートです。
やりたいこと
APIで発生したエラーは、最終的にResponseの形で、以下の例のように返されます。
GET /api/exc/10/?hakurei=reimu HTTP 404 Not Found Allow: GET, HEAD, OPTIONS Content-Type: application/json Vary: Accept { "detail": "Not found." }
(実用性はさておき)例えば、下の表示のように変更したいです 特に、フロント側でエラーの結果などやフラグを表示したい時、自由にカスタマイズできると助かると思います。
GET /api/exc/10/?hakurei=reimu HTTP 404 Not Found Allow: GET, HEAD, OPTIONS Content-Type: application/json Vary: Accept { "detail": "Not found.", "status_code": 404, "patchouli": "ロイヤルフレア", "params": { "hakurei": "reimu" }, "kwargs": { "pk": 10 } }
本記事での各種ファイル
サンプルアプリの、本記事に関係する部分のコードです。
api/urls.py
app_name = 'api' urlpatterns = [ path('exc/', ExceptionView.as_view(), name='exc'), ]
- config/urls.py
urlpatterns = [ path('admin/', admin.site.urls), path('api/', include('api.urls')), ]
- api/views.py
class ExceptionView(APIView): @csrf_exempt def get(self, request, *args, **kwargs): from rest_framework.exceptions import NotFound raise NotFound
カスタム処理を挟むことができる場所
基本的に、エラーが発生してからレスポンスとして返すまでの流れは以下の通りです。変更できる箇所は、(1)~(3)に分類できると思います。
エラー(Viewの中) ➡︎ 例外をraise(2) ➡︎ Exception Handler(3) ➡︎ レスポンス
⬇︎(1) レスポンス
(1) エラーから直接レスポンスを作る
DRFの公式チュートリアル序盤でも出てきた方法です。 単純に、直接自分でレスポンスを書いてしまえば、どんな形でも返すことができます。
注意点として、Exceptionをraiseしていないため、Exceptionのハンドラ(後述)を経由する処理が適応されないという点があげられます。 また、serializerのバリデーションエラーなども自分で渡してあげる必要があります(下リンク参照)
class ExceptionView(APIView): @csrf_exempt def get(self, request, *args, **kwargs): return Response(data={'error': 'エラーです'}, status=404)
GET /api/exc/10/?hakurei=reimu HTTP 404 Not Found Allow: GET, HEAD, OPTIONS Content-Type: application/json Vary: Accept { "error": "エラーです" }
(2) 既存、もしくは作成した例外をraiseする
DRFは、APIViewを継承しているView(正確にはhandle_exception
メソッドが実装されているView)内部で例外が発生すると、それをキャッチしてレスポンスを作成し、返してくれます。
例外は、APIExceptionを継承したもの、もしくはDjangoのHttp404 または PermissionDenid Exceptionが対象となります。
既存の例外
rest_framework.exception
下にあります。api/views.py
class ExceptionView(APIView): @csrf_exempt def get(self, request, *args, **kwargs): from rest_framework.exceptions import NotFound raise NotFound(detail='変更したメッセージです', code='変更したコードです')
detail引数を変更することで、メッセージを変更できます。デフォルトでも、例外の種類に応じたメッセージが表示可能です。
GET /api/exc/10/?hakurei=reimu HTTP 404 Not Found Allow: GET, HEAD, OPTIONS Content-Type: application/json Vary: Accept { "detail": "変更したメッセージです" }
カスタムの例外
APIExceptionクラスを継承し、以下のように3つのfieldを定義すればOKです。 https://www.django-rest-framework.org/api-guide/exceptions/#apiexception
- api/views.py
class PatchouliUnavailable(APIException): status_code = 503 default_detail = 'サーバーが物理的に焼け焦げています' default_code = 'service_unavailable_desune...' class ExceptionView(APIView): @csrf_exempt def get(self, request, *args, **kwargs): raise PatchouliUnavailable
結果です。detailの内容と、2行目が503となっています。 default_codeについては、標準のハンドラ(後述)ではレスポンスに反映されません。
GET /api/exc/10/?hakurei=reimu HTTP 503 Service Unavailable Allow: GET, HEAD, OPTIONS Content-Type: application/json Vary: Accept { "detail": "サーバーが物理的に焼け焦げています" }
(3) Exception Handlerをカスタマイズする
DRFを使うなら、ViewSetやbilt-inのViewを使うと思います。そうすると、raiseされている例外がどこから発生しているかは、ソースコードを見なければわかりません。また、全ての例外➡︎レスポンスについて、情報を付加したいこともあると思います。 そのような場合には、例外➡︎レスポンスの変換をしているハンドラを変更することで対応できます。
rest_framework/views.py
にある、exception_handler
関数が、例外➡︎レスポンス、の変換を最終的に行います。
exception_handler
関数をオリジナル関数に差し替えることで、グローバルに変更を適応できます。
また、exception_handler
関数を実際に呼んで利用している、get_exception_handler
関数を各Viewで上書きすることで、個別のviewの挙動を変更できます。
https://www.django-rest-framework.org/api-guide/exceptions/#custom-exception-handling
- オリジナルのハンドラの例です。ステータスコード、リクエストのGETパラメータ、URLにエンコードされたpk情報、任意の文字列をレスポンスのbody(response.dataに、dictとして渡せます)に含める処理を追加しています。
- 表示には利用しませんが、exceptionオブジェクト自体を持たせておくと、テストの時などに便利です (exception_obj)
def patchouli_exception_handler(exc: Exception, context: dict) -> Response: # レスポンスヘッダをセットするなどの処理が必要なので、オリジナルのハンドラでレスポンスオブジェクトを一旦作ります。 response = exception_handler(exc, context) if response is not None: # レスポンスのbodyに入れたいものは、data属性(dict)に入れる。 response.data['status_code'] = response.status_code response.data['params'] = context['request'].query_params response.data['kwargs'] = context['kwargs'] response.data['patchouli'] = 'ロイヤルフレア' # exceptionオブジェクト自体を持たせておくと、テストなどで便利です。 response.exception_obj = exc return response
上記で書いたハンドラを、Viewにセットしていきます。1. globalに適応する、2. 個別のViewに適応する、の2通りあります。なお、ExceptionをResponseに変換するDRFの仕組みを利用するには、APIViewを継承している必要があります。なので、そのようなViewを使うか@api_view
デコレータを使ってviewを修飾しておく必要があります。
グローバルに設定する
作成したハンドラへのパスを書きます。ハンドラはどこにおいてもよいですが、app(今回は"api"という名前)の直下にutilsを作りました。
- config/settings.py
REST_FRAMEWORK = { 'EXCEPTION_HANDLER': 'api.utils.patchouli_exception_handler', }
個別のViewに設定する
公式ドキュメントに記載はありませんでしたが、get_exception_handler
関数を個別に上書きすることができます。またこの関数を呼んでいる、ラッパー関数のhandle_exception
を上書きすればさらにいろんな修飾を加えられます(code not shown)。
(from-importの位置がよくないですが、利用箇所を明示するためにこのように書きました。)
- api/views.py
class ExceptionView(APIView): @csrf_exempt def get(self, request, *args, **kwargs): from rest_framework.exceptions import NotFound raise NotFound def get_exception_handler(self): from api.utils import patchouli_exception_handler return patchouli_exception_handler
結果
GET /api/exc/10/?hakurei=reimu HTTP 404 Not Found Allow: GET, HEAD, OPTIONS Content-Type: application/json Vary: Accept { "detail": "Not found.", "status_code": 404, "patchouli": "ロイヤルフレア", "params": { "hakurei": "reimu" }, "kwargs": { "pk": 10 } }
そのほかの注意点
500エラー
DRFが用意しているAPIExceptionのどれにも該当しなかった場合、Djangoに処理をゆだねます(例外をraiseする)。Djangoの500エラーのテンプレートが利用されることになりますが、これをJSONレスポンスに変更することもできます。 https://www.django-rest-framework.org/api-guide/exceptions/#generic-error-views
configの直下のurl.pyに、以下を指定するだけです。
- config/urls.py
urlpatterns = [ path('api/', include('api.urls')), path('index/', TemplateView.as_view(template_name='index.html')), ] handler500 = 'rest_framework.exceptions.server_error'
もともとcurlを叩くと以下のようでしたが
$ curl -X GET localhost:8000/api/exc/ <!DOCTYPE html> <html lang="ja"> <head> .....
上記のhandler500にserver_errorのviewを設定したことで、以下のようになります。
$ curl -X GET localhost:8000/api/exc/ {"error": "Server Error (500)"}
ただし、このようにするとこのproject全体の500がJSONレスポンスになってしまいます。 もし、apiと同一project内部で別のappにより、Djangoで通常のテンプレートを表示しているような場合にはContent-Typeを見て、処理のviewを分岐する必要があります。
どこに処理を挟むかのベストプラクティスがあれば、教えていただけると幸いです。個人的には、500エラーの際には上述したハンドラでresponseがNoneとなるので、それを見て特定のviewを返すのが良いと思っていますが...。
- api/utils.py
def course_500_exception_handler(exc: Exception, context: dict) -> Response: response = exception_handler(exc, context) if response is None: from rest_framework.exceptions import server_error return server_error(request=context['request'])
あとがき
ソースコードについても、あとでまとめようと思います。
読書感想文: リーダブルコード
- 個人的総括
- 1. 理解しやすいコード
- 2. 名前に情報を詰め込む
- 3. 誤解されない名前
- 4. 美しさ
- 5. コメントすべきことを知る
- 6. コメントは正確で簡潔に
- 7. 制御フローを読みやすくする
- 8. 巨大な式を"""分割"""する
- 9. 変数と読みやすさ
- 10. 無関係の下位問題を抽出する!
- 11. 一度に一つのことを....
- 12. コードに想いを込める
- 13. 短いコードを書く
- 14. テストと読みやすさ
- 15. 分、時間カウンタを設計実装する。
- 質問/ メモ
個人的総括
- この本でいう"リーダブル"は何か、どうすれば達成できるのか?
- 修正しやすい == 保守しやすいコード => 後から参加したメンバーや遠い未来の自分も保守できるくらい。
- 改修に必要な情報は目立たせて理解させる。不要な情報は、混乱, 疲弊の元。-> 見えないor見なくていいとわかるようにする、書かない。
- (修正できるくらいの深さまで)"理解"し易い
- 単純に読むのが楽(-> 見た目(命名など), ロジック(「説明」に沿った形))
- 分離している (ある変更において読むべき場所が少ない)
- プロジェクトのロジックを追いやすい
- 修正箇所が一箇所でいい。
- 実際に修正, 機能追加しやすい
- 関数を再利用しやすい(繰り返さない)
- 読めるだけでなく、機能追加が簡単にできるか?を意識する
- 書いた時はシンプルに見えても、機能追加で複雑度が増すような構造はNG : 関数への分離で解決することが多い
- 「頭がいいコード」は、同じ書き方で機能追加が絶望的に難しい
- 統一感が消える。
- 統一感がなくなると、読めないしどの方法で書き足すかも不明になる。
- 章をまたいで繰り返されたアドバイス
- 一つのモノには1つの仕事をさせる。複数詰め込まない。
- 他人の視点を考える("他人" -> 半年後の自分自身かもしれない)
- ひどいコードとは、"触りたくない"(読む, 修正, テスト追加, 等)コードのこと。
- コードを、他のコードになるべく依存させない(分割, 変数のスコープを小さく, 標準API使う)
そのためのアドバイスの個人的分類
- 1部: 表面上の改善
情報量/読解のエネルギー(文字数,見やすさ) のratioを高くすること が基本的な方針。その具体的なアドバイス。
- 読んでから理解するまでの時間を短くできる(1章の目標)。
- コメントは文字数を増やす代償以上に情報を与えないとダメ(コードだけでわかる変数名ならそれが一番いい。)
2部: ループとロジックの単純化
気がかりなことを残したまま、読み進めるコードは避ける が基本的な方針。大事なことは先にいう。
3部: コードの再構成
- 修正において読むべき部分を減らす(関係ない部分の隠蔽, そもそもコードを減らす, デフラグ)指針を述べてる。
- ひどいコード」は修正しづらいコード = 保守しづらいコード:: 無関係の下位問題を抽出する!
- 大切でない情報は隠す,大切な情報は目立たせる: 人間が持てる荷物は限られてるからね。concentrate.
- e.g)国の名前をプリント元のやつ-> そんな悪くないと思ったけど、条件追加を考えると厳しい。
- 分離-> 無関係な下位問題があると変更難しくなる。
- 触ったらアカンとこはまとめたほうが変更し易い
- 関係ないからってイメージでスルーして追加しやすくなる。
- プロジェクト自体のコードだけにダイエットできてるから、プロジェクトのコードを追加しやすい
- 最低限であるほどよい-> contextと、general, specific, 必要十分の話。
- 余分なコードはゼロではなくマイナス。
- 書くだけでなく、保守やDoc化の手間も意識する。>> 保守し易いコード
- コードを説明するのではなく、説明をコードにする
- テストし易いコードを書く-> 疎結合, 1:1が実現し易い。-> RESTってこれっってことやんな?
- 明確なインターフェイス
- セットアップ, ステートに依存しない
1. 理解しやすいコード
- 他人が読んで理解するまでにかかる時間が短いことを指標として書く。
2. 名前に情報を詰め込む
- Q: generalよりspecificを選ぶ ということっぽい。機能をより特定できた方がいい。(よって、長い名前にもなりうる。)
Q: 複数形大事。複数形の引数なら、listをとるとか予測できる。
明確な単語を選ぶ。
# 明確な単語を選ぶ。 getPage() # NO. どこからPageをgetするかよく分からない。 fetchPage() # Yes. ネットから取ってくる。 downloadPage() # Yes. 同上。 size() # NO. なんのサイズか不明。 height() # YES NumNodes() # YES MemoryBytes() # YES Stop() # No. 止まることしかわからん(情報 x 1) Kill() # YES. 止まるし、消えることがわかる。 Pause() # YES. 再開可能だとわかる。
tmpなどの汎用的なものを避ける。
左右の値を入れ替えるような、「一時的によけておくだけ」という変数ならtmpでいい。一時的によける、以外の機能がないことを示せてるので良い名前。
- 保持しているものにそれ以外の意味があるなら、それを名前にするんや。
イテレータのi, j, kはよい。「イテレータです」って意味になるから。
- でも、membersならmi, menbers_iとかにする方がわかりやすい。
抽象より具体的な名前がいい。
TCP/IPポートがリッスン可能か調べる関数
- OK: CanListenOnPort() 具体的!
- NO: ServerCanStart() それ以外の目的でもつかうかもしれん
直行する概念をまとめてはいけない 個人的に重要. Unixっぽい思想?一つのモノに完璧な一つの仕事をさせる。
- No: localテストのため、localのDBをつかいdebugを表示するコマンド:
--run_locally
- Yes: localのDBをつかう
--use_local_db
とdebugを表示する--extra_logging
- No: localテストのため、localのDBをつかいdebugを表示するコマンド:
名前に情報を追加する
値の型や単位を間違えると危険なところに使う。
hex_id
: idが16進数であることが重要な場合。start_ms
: ミリ秒、単位を追加する.計算する時などは単位を変数につける。delay_secs
: 秒。size_mb
: これはメガバイトつけたから、sizeってつけてもいいのかな?
名前の長さ
変数を使うスコープが広いほど長い名前でいい!! 旅行は遠くへ行くほど荷物が多くなる。
- でも可能なら短い方がいい!
- 不要な情報を捨てる:
ConvertToString()
->ToString()
これでもわかる!
- 不要な情報を捨てる:
省略形は、慣習的なもののみつかう(document-> doc)
名前のフォーマットを守る
pythonならPEP8守る。全て大文字なら定数、とかね。
- js: jQueryのオブジェクトを束縛する変数は
$
から始める$all_images = $("img")
など。 - html: idはアンダースコア、クラスはハイフンで区切る まじ?
3. 誤解されない名前
filter(x < 0)は悪い名前らしい : filterして、0以下をとるのか、0以下を除外したのか不明だから。
- OK :
select()
: filterして0以下を選んでる - OK :
exclude()
: filterして0以下を除外してる
- OK :
textの最後を指定文字数消して「...」をつける
- NO: Clip(text, length) : lengthだけ削るのか、lengthだけ残すのか不明。
- YES: Truncate(text, max_chars) : max_charsだけ残すとわかる。また、charにしたので文字数だとわかる
範囲を表す奥義
- 限界値はMAX, MINを先頭につける!
max_items_in_cart
: 許容される値(=<)を示す。 - 範囲はfirst, lastを使う! first=1, last=4なら、1,2,3,4 の集合。
- start, stopだと、stop=4のとき、1,2,3か1,2,3,4がよくわからない。(4を処理して止まる?4を見て止まる?)
- スライス的な(最初含み、最後含まず)範囲は、begin, endを使うという慣習がある!!
- 英語には、スライスでよくやる「最後」含まずの表現がない。この時の引数はbegin/endを使うと決まっている!
- この表現は例えば、「16日のイベント」を、16日0:00~16日23:59 とするより、 16日- 17日(end)の方が「16日全体」を表しやすいという理由で使われとるらしい。包含/排他的範囲っていうらしい。
- 限界値はMAX, MINを先頭につける!
Bool値はを持つときは、先頭にis, has, can, shouldをつけるとよい。
- 誤解を避けるため、is...がTrue、みたいに読めるようにする。
- boolに否定語は避ける。
disable_ssl
など。否定の否定になると読みにくい。use_ssl
にしする。
イディオムが存在する
- get(え、つかうんかget..)は軽量な取得って慣習があるらしい。
- 計算して返すなら、computeをつける
4. 美しさ
これはPythonの強いところ。文法通りに書けばそれだけで綺麗だし、PEP8守ればもっと綺麗。
読み手が読み慣れてるパターンと一貫したレイアウトを使う(自然とできる)
- 似たコードは似てる見た目にする(!!!)
- 関連するコードはまとめてブロックにする
- たとえばimportするときに種別でブロック分ける、とかそういう処理の分類を空行入れるような作業。
- たとえばある一連の処理で、区切りごとに空行とコメントを入れる # dataをwebから取得する とか。
5. コメントすべきことを知る
- コメントの目的は書き手の意図を伝えること。
コメントは「コードを増やす」という代償を払っているので、代償に見合うリターンを持たせないといけない!
- コードから""""""すぐに""""""わかることをコメントに書かない
- コメント読んだら早く理解できるなら、書くことを検討できる(コード自体を分かり易くした方がいいけど。)
- 優れたコード >> ひどいコード+優れたコメント
- コードから""""""すぐに""""""わかることをコメントに書かない
コードを書いているときに持っている、大切な考えを入れる!!!
- "このデータだと、ハッシュテーブルよりバイナリツリーが40%早かった"
- "このクラスは汚くなっている", "...で整理した方がいいかもしれない"
- 上記は優れたコメントらしい。 後から見た人にとって役立つ情報だから。(これがなければ、考えることに余計な時間つかっちゃう。)
- コードの欠陥にコメントをつけるイディオム
これからコードをどうしたいのかを書くのが大切。
TODO:
後で手をつけるFIXME
既知の不具合があるHACK
あまり綺麗じゃない解決策XXX
危険。大きな問題がある。
定数へのコメント
- わいの書いたorder に1000(実質ラストになる)みたいなのはokらしい。
- コメントの書き方も、「最後に来る十分大きな数」みたいなんでokぽい。
- 定数を決めたとき、頭の中で考えていたことを書くのは重要、と述べている。
全体像のコメント
- ファイルの最初とかに書く。特に、これは単なるキャッシュとか、そういう情報があると読み手は嬉しい。
- Q: documentの説明書ってやつやな。
p67. 例: 処理ブロックにコメントを書くとよい。コメントの内容を関数名にして処理を分けるとさらに良い.
- ⬇︎Good!!
def generate_user_report(): # このユーザのロックを獲得する .... # ユーザの情報をDBから読み込む ... # 情報をファイルに書き出す ... # ユーザのロックを解放する
- ⬇︎Best!
def generate_user_report(): obtain_user_lock() retrieve_user_info_from_db() write_to_file() release_user_lock()
WHATよりWHYを書くというアドバイス
- 本書では、役にたつならなんでも書こう というスタンス。
推敲を推奨している.
- コメントも、まずはクソ文を書いて、それを推敲すると手が進むことを述べている。
6. コメントは正確で簡潔に
- ratioが大切。 情報量 / 領域(文字数) :: これprincipalだな。
- dockstringを進めてる!(処理の例示) C++ではdocstrはないので、普通のコメントという形になる。
- わいも思ってるけど、pythonとか引数名を渡せる言語では、関数呼び出し時に引数を明示した方がいい.
- C++とかではインラインコメントをつかう。
Connect(/* timeout_ms = */ 10)
など。
- C++とかではインラインコメントをつかう。
- ジャーゴンを推奨している。 短くて正確, 密度の高い専門用語をコメントで使うよう推奨している。
7. 制御フローを読みやすくする
比較の順番
- 左が調査対象(変化する), 右が比較対象(変化しない, ただの定数)
while(bytes_revieved < bytest_expected)
など。- 英語の語順と同じにする(対象値がless than 10なら..., 10がmore than 対象値なら...)
- 英語的には「 < 」の記号も自然言語的に読むことが起因してるぽい。
- これ英語特有の問題じゃね..?
if/elseの条件と処理の順番
- 以下は両立しないこともあるので、絶対ではない。
- (a != b)や(!a)よりも(a == b)や(a)を使う!(!aを見ると、aを考えてしまう。なのに!aを先に処理するのはNG)
- 単純な処理の条件を先に書く.(処理内容が短い方を先に読みたい)
- 関心を引く条件、目立つ条件を先に。(e.g.
if not file: .....
など。否定だが、fileないときの方がまず知りたい。)- 処理が長いならfileない条件も後に来る。
- ファイルがない時の処理がエラーログ出力だけ(単純な処理)なら先。
- つまり、3つの原則でより多くを満たす方にすりゃええんやね。
-
- 三項演算子が「処理をうまく表してる」ときに使う。そうでなければif/elseでかく。
do/whileはダメ というのが本書のスタンス。
- 条件は前もって知らされる方がいい。
- 他の章でも言ってるが、不確定な情報をもったまま読むことを避けたいという意志が強い。(if/elseの条件の話や、比較の順: 見たい値が何か知らないまま定数を心のメモリに入れたくない。)
関数から早く返す
- returnはいくつ書いてもいい。とにかく早く返す。
- 早く返すことにより、ifのネストを減らせる可能性も高い -> ガード節
- ガード節 :(多分) 簡単なケース(処理の対象外のケース?)を関数の一番上で処理-> returnやcontinue, breakしておいて、後からチェックしないといけないifの条件文を減らす手法。
- 「ガード節」という書き方。 - Qiita](https://qiita.com/kouyan/items/7b8b456b626447a1e24e)
関数のクリーンナップコード
- 関数中で絶対実行したいコード-> これを、returnを最後にすることで実装するのはほんまにクソ。
- pythonには try-finally および withというクリーンナップ文ある
8. 巨大な式を"""分割"""する
- 説明変数 : 一度いい名前の変数に束縛して、それを比較に使うなど。
- ('username = line.split()[0].strip()')など。
- 式の結果が"userのname"だとよくわかる。
- ダメな例:
now = datatime.datetime.now()
素のままでもわかりやすかった。無駄に変数ふやした。(9章)
- 要約変数 : ⬆︎は式の説明だが、これは長い条件式をまとめる のが目的
user_owns_document = req.user.id == doc.woner.id
とししてif(user_owns_document)
-> 説明は不要( == 型も見ればわかる)けど、何度か使うならこれ。(読む量減る!!)
ドモルガンの法則をうまく使う
- ん?これわかりやすいかな...?
「頭がいい」コード(自己満足の複雑な1 lineコード)は絶対書かない。
小さい範囲なら、読みやすくするためにそのスコープ用の短縮系を宣言することもあり。
- 短縮系をそのままに、長いoriginalバージョンの名前を変更(一箇所で済む!)できるメリットもある
- 改修のしやすさがここでも出てくる。関数化は、改修の時変更箇所を減らせるのです。
- 短縮系をそのままに、長いoriginalバージョンの名前を変更(一箇所で済む!)できるメリットもある
- 問題を「反対」にしてみたりして、ロジック自体を単純にすることを試みる(いつもやっとるわ)
9. 変数と読みやすさ
- 中間コードを保持しすぎない。
- 説明変数の対立概念。タスクは早く完了した方がわかりやすい.
- 中間コードのままでわかるなら、変数つけない方がいい。
- 値をその場でreturnすることで中間変数は減らせる
- 制御フロー変数を削除する
- わいが"フラグ"やと思ってるbooleanや。
- フローの制御のためだけの変数は、ロジック改善して消し炭にする。
- 変数のスコープを縮める
- その変数が効いてる範囲は小さい方がいい(globalは最悪: 書いた人は"ここでのみ使う"と思っても、読む人はfileの最後までその変数のことを気にしなくてはならない!)
- 一度に考える変数は少ない方がいい 3つ以内とか。
- Q: 変数をたくさん定義してるなら、多分そこは分離して小さなスコープにまとめられると思う!
- クラスの変数(メンバ変数)はミニグローバルで危険
- 全てのmethodが見えうるので、追うのがクソ大変(使える範囲(行数)がクソ長くなりうる)
- 可能なら使用メソッド内部のみのlocal変数にscopeの格下げするのがよい。
- staticメソッドは意味がある 「メンバ変数と無関係なメソッド」だとわかるので.(クラス内の他のメソで利用する(self.static_fucn())が、メンバ変数は使わない..みたいな状況かな)
- p118 jsのクロージャを使ってのスコープ極小化は重要(global変数をプライベート変数にする)
- globalで使うと、この箇所でしか使ってなくても、読み手はfileの最後までこの変数を気にせなあかんくなる。
- このjs例の、クロージャの外側関数を定義->即実行()ってのpythonでもやるんか?
- Q: そもそもPythonも、クロージャは外側の関数の変数を記憶した関数という文脈でつかうぽい。【Python】クロージャ(関数閉方)とは - Qiita
- 外側の関数で定義した変数を、内側の関数に覚えさせておくという見方がわかりやすい?(内側の関数で、変数で渡した値がハードコードされたようなしくみになる。)
- Pythonだと、内側の関数(こっちがクロージャという)内部でnonlocal val宣言しておくと、外側関数(エンクロージャ)の変数を読むだけでなく書きができるようになるやつやね。ステートありなのでclassと同じ概念か。
- pythonでは即実行ってどうやる?lambdaか?
submitted = false; // こうするとglobal変数になるらしい(varなし) var submit_form = function (form_name) { if (submitted) { return; // 二重投稿を防ぐ(初回のみ実行) } .... submitted = true;// 値を変更しとく }
var submit_form = (function () { var submitted = false; // 下の内側関数だけがsbumittedにアクセスできる。 return function (form_name) { // 内側のfunctionの結果が返ってsubmit_formに束縛される if (submitted) { // pythonと同じく、外側関数の定義スコープでのみ"submitted"が使える。 return; } .... submitted = true; }; } () ); // この()に注目。 function() {} () って形で定義後即実行になってる。
変数のスコープを縮める(続き)
- (p120)pythonの、ifとかのブロック後でも定義した変数がずっと残る問題への対処
- 明確に定義する。
example = None
を処理的に不要でもbaseブロックに書くのは、宣言不要といえども意味がある。 - 中間コードをなくす(処理を関数にまとめ、値を保持せず即実行すれば変数宣言しなくて良い)
変数は一度だけ書き込む : jsのconst、有能やんけ!!
- pythonでも、変数は書き換えない方がいい。
- immutableなものを使えるときは, immutableなものをつかう!!
- フラグ的な変数も、その意味で(変更前提の定義)、あんまり使わん方がいい。
10. 無関係の下位問題を抽出する!
- プロジェクト独自のコードから、汎用的なコードを除く。
- プロジェクトコードを読むときは、プロジェクトのことだけ書いてあるといいね!
- これは、積極的にやるべき行為らしい。
- 分割しすぎはよくないけれど、意味のある塊なら抽出する。
- Q: 処理ベースじゃなくて、目的ベースやと思った。抽出したヤツに名前がつくなら多分OKやと思う。
方法
- 関数やコードブロックの高レベルの目標を見つける-> 関数名。実現したいこと。
- コードの各行を見る。
- 高レベルの目標を直接みるコード-> そのまま
- 無関係の下位問題をみるコード -> 検討の余地あり。
- 無関係の下位問題をみるコードがある程度あれば抽出する。
無関係の下位問題: args-> rturnできる小さなスコープの問題。高レベル問題を解く際の自己完結した部品部分。
- Q: これいうたらブロック中で段落にまとめられる、処理の一単位やね。
- その中でも、自己完結しているもの、だな。
- 文字列や数値の加工だよね、ほとんどの場合。
- e.g. 最近接距離をarrayから求める際の、余弦定理部分。
- e.g. 文字のformat整理部分(pretty print->
format_pretty()
関数を作ると良い)
- Q: これいうたらブロック中で段落にまとめられる、処理の一単位やね。
可読性以外のメリット
- 関数を独立させることで、テストや改善がやりやすい。
utilを作る?!
util/
をまとめること進めてるけど、それってYO!どうやって管理するんだYO!
プロジェクトとの関係
- プロジェクトに依存しない(外部ライブラリにもできそうな)util関数はめちゃめちゃGOOD.最高。
- プロジェクト自体のコードを小さくできる!
- プロジェクトに特化してても、分離に意味はある。
- p137のpythonコードはかなーり勉強になる。
-
CHARS_TO_REMOVE = re.compile(r"['\.']+")
をfile上部で定義してる。 -
make_url_friendly()
は、projectのその場所で特化した文字列処理。(usernameをurlにする) -
make_url_friendly()
は、utilにおいてもいいし、元のファイルと同じ場所においてもいい。- これは後から決めてもいいらしい。とにかく抽出するのが大事とのこと。
-
- p137のpythonコードはかなーり勉強になる。
- プロジェクトに依存しない(外部ライブラリにもできそうな)util関数はめちゃめちゃGOOD.最高。
-
- 自分がやりたい1アクションを理想的なwrapper関数にする」感じかな。
- グルーコード: Glue Code: 互換性がない部分を結合するためのコード。ORMとか。glue: 接着剤
- 関数の事前/事後処理など。
- これは積極的に抽出すべき。
11. 一度に一つのことを....
- (わいもやってしまうけど)一つの関数やブロックは、一つの処理(Q: 命名可能?)だけ扱うべきや。
- コードのデフラグという見方!!
- 関数に限らない。ブロック中の、一段落のロジックにも言える。
- タスクは異なる関数、もしくは領域に分割する。
ちょっとよりみち
- 順番に値を見ていき、最初の奴を使う。全部外れなら、defaultを使う。
- こういう動作は関数にしたら良いかも知れん。ロジックで書くときはdefaultを定義->書き換えをやる
- 「デフォルト値設定」ってのは、そういうタスクなので、複数あれば全部まとめて最初に書くのがいいらしい。
var second_half = 'Planet Earth'; //デフォルト値 if (country) { second_half = country; } if (state && country === "USA") { second_half = state; } `
12. コードに想いを込める
- 読み手に「わかりやすく」説明するコードについて、そのほかのような区分。
- コードを説明するのではなく、説明に合わせてコードを書く。説明しやすいコードを書く。
- 説明しやすさを優先して、先述のif文のルール(気になるモノを先に返す)を破ることもあるぽい
- 数値のハードコードにつながる
- △
page += 1
「次のページ」という説明とズレる。 - ◎
page = current_tip.next()
「次のtipに移動する」メソッドだとわかる感じになってる。
- △
- p162 え?iteratorって、要素にアクセスするだけでnext...いや、呼ばれへんわ
- ラバーダッキング
13. 短いコードを書く
- 使わないでいいコードは絶対に書かない かいてしまいがち。
- 書いて終わりちゃうんやぞ?
- 保守やDoc化の労力、デバッグの労力がある。少ないことはいいことだ!!!!
- この章、generalとspecific, contextの話だ!!!
- 自分が使う文脈にspedificな処理を書くべきで、generalである必要はないということや。
- それが最小限の労力ということになる(保守やDoc化! 書くのは簡単だとしてもね)
- 標準ライブラリや、サードパーティライブラリをしっかり知る!
- Unixツールボックスを使う.
- pythonだと、wrapperクラスあるよね。
14. テストと読みやすさ
- 本書的にはテストは読みやすくあるべきという立場。テストを公式文書と考えてもいいくらい。
- 保守しやすいテストをオススメしている。
- 追加, 変更が簡単にできること! これは本当に、常に重要。
- POINST: 大切でない情報は隠す。大切な情報は目立たせる
- p183: 最小のテストを作る:
CheckScoresBeforeAfter("...", "...")
がある。- これ、PythonのUnittest系なら、self.assertEqual()を内部で呼ぶ関数を作ることになるのかな...
- 複数の値を順に試すのは、Fixture使うんかね。
- とにかく、入力値を変えるだけでなんどもテストできる関数をどこかで定義する方法を考えた方がいい。
- コードを"完全に"テストする、""最も単純な""入力値の組み合わせを考えないといけない!
- たとえば、"負の数"の例に"-1"以外を使うのはダメ!!(余計な情報となる)
- 逆に読むときは、"-1"は負の数の例、負の数でありさえすればいいという例になる。
- 一つのテストで一つのケース
- 負の数の処理の時はそれだけを。降順ソートならそれだけを。
- そういうテストを、複数並べるのが良いテスト。(行数は増えるが、「理解が短時間でできる」)
- 芸術的な組み合わせはNG!
- テスト関数の名前は長くなっていい。なぜなら、他の関数から呼ばれまくったりしないから。
- 単なるそのテストのドキュメント(コメント)としてテスト関数は名付ける!
- たとえば、"負の数"の例に"-1"以外を使うのはダメ!!(余計な情報となる)
テストしやすいコードを書く
疎結合の意味とは
15. 分、時間カウンタを設計実装する。
- インターフェイスを先に決める
- クラス名, メソッド名を先に決定できるし、これならごちゃごちゃすることを先に防げる。テストも楽。
- コメントやメソッド名のリファクタリングもこの時点でしてしまう!
- 同僚に、クラスとメソッド名とコメントを見せて聞く。 「これはどんなメソッドだと思う?」
- for文でのiterはiでいいけど、逆回しならritとか、"r"を最初につけるとよい。
- 50行の読みにくいコードより、100行の読みやすいコードが優れている
- 理解するまでの時間!!なんどもいうけど!
- タイムスタンプの例では、やはり機能を追加可能か?という観点からリファクタしてる。
- コミットも、「一つの変更」について「1コミット」がよい!!
- れいのGitLabの提案モードを使ったリファクタはかなり重要なのでは?!
- 本書では"提案コミット"といって、新しくコミットすることをすすめとる。
- れいのGitLabの提案モードを使ったリファクタはかなり重要なのでは?!
質問/ メモ
- 分割した関数ってどこに置けばいい?とくにPythonやJSでそういう慣習をまとめた本ある??ってかいいライブラリある?(構成が美しくて読むと勉強になるライブラリ)
p135
汎用コードをたくさん作る
Projectのutil関数って、まとめた後どうやってその"存在"を報せればいいんや??
- 僕もコード書いていて、「こういう処理」が欲しいと考えはしたり、他のview関数のコードを見ている時に便利なutilを見つけたりはしたけれど、そこを見てなければ自分で定義してた
- READMEか、なにか城跡的にか、すでに作られたutil関数みたいなのはどこで見つけられるんや...
- 究極この手の問題は、全て外部ライブラリに依存した方がいいのか?
staticメソッドって、結局classの外に定義したらあかんの?どんな時にclass内部に定義するのん?--Pythonの「@staticmethod」はどのように役立つのか - モジログ
- inheritする時、staticメソに定義しておけばそのstaticメソだけoverrideすれば良いという利点がある(instance,class メソッドでもいいけど、そこはリーダブルコードのようにinstanceの変数使ってないことを明示できる利点があるね!)
いうて、「ダメ」と書かれた名前つことるときもあるし、使うにしても他の観点から読みやすくしたり(規則を作ったりね)しとる。