diadia

興味があることをやってみる。自分のメモを残しておきます。

データの一般化

プログラミングの基礎を最近読んでいます。OCamlを使ってプログラミングの考え方を学ぶっていうのがこの本のコンセプトです。
https://www.amazon.co.jp/dp/4781911609/

データの一般化

本書120ページあたりから。概要は関数を作っていると似たような関数ができてくる。これらの関数を一般化することでより多くの場面で使うことができるようになる。したがって一般化は大事みたいな内容。

学んだことをpythonで表現

多数決の意見のデータが以下の辞書型データとして存在している。
{"name":"satou", "iken":"yes"}
この辞書型データを格納したリスト型データをopinion_listとする。

opinion_list = [
    {"name":"satou", "iken":"yes"},
    {"name":"yasuda","iken":"no"},
    {"name":"takano","iken":"yes"},
    {"name":"ono",   "iken":"yes"},
]

このリストデータを読み込んで意見が"yes"の人数をカウントするとともに、別の関数として"no"のものも定義してみる。

def count_yes(lst):
    n = 0
    for ele in lst:
        if ele["iken"] == "yes":
            n += 1
        else:
            continue
    print("{}{}{}".format("yes","の人数は",n))
    
    
count_yes(opinion_list)
yesの人数は3

noのものは以下の通り。

def count_no(lst):
    n = 0
    for ele in lst:
        if ele["iken"] == "no":
            n += 1
        else:
            continue
    print("{}{}{}".format("no","の人数は",n))


count_no(opinion_list)
noの人数は1

次に両者を一般化して引数にyes,noを加えることで同じ結果を出力する関数countを設ける。また引数を"yes"として実行結果を確認する。

def count(lst,iken):
    n = 0
    for ele in lst:
        if ele["iken"] == iken:
            n += 1
        else:
            continue
    print("{}{}{}".format(iken,"の人数は",n))



count(opinion_list, "yes")
yesの人数は3

これで引数を変えることでカウントできる関数を作ることができた。しかしながら今までcount_yesをスクリプト上に書いてきた場合、修正が面倒になる。これについても解決できることが判明。それは一般化した関数countを使って再度count_yesを定義しなおすことである。

def count_yes(lst):
    count(lst,"yes")
    
    
count_yes(opinion_list)

結果はもちろん同じになる。

yesの人数は3

一般化した関数を使った場合使わない場合のコードの比較

使わない場合のコード

opinion_list = [
    {"name":"satou", "iken":"yes"},
    {"name":"yasuda","iken":"no"},
    {"name":"takano","iken":"yes"},
    {"name":"ono",   "iken":"yes"},

]

def count_yes(lst):
    n = 0
    for ele in lst:
        if ele["iken"] == "yes":
            n += 1
        else:
            continue
    print("{}{}{}".format("yes","の人数は",n))

    
def count_no(lst):
    n = 0
    for ele in lst:
        if ele["iken"] == "no":
            n += 1
        else:
            continue
    print("{}{}{}".format("no","の人数は",n))

いままでこの書き方をしていた。この書き方しか知らなかった。

一般化した関数を使わう場合のコード

opinion_list = [
    {"name":"satou", "iken":"yes"},
    {"name":"yasuda","iken":"no"},
    {"name":"takano","iken":"yes"},
    {"name":"ono",   "iken":"yes"},

]

def count(lst,iken):
    n = 0
    for ele in lst:
        if ele["iken"] == iken:
            n += 1
        else:
            continue
    print("{}{}{}".format(iken,"の人数は",n))
    

def count_yes(lst):
    count(lst,"yes")
    
def count_no(lst):
    count(lst,"no")

コードをかなりすっきり書くことができた。

再考:DetailViewの構造

DetailViewで詳細ページが表示できる理由

class ProductDetailView(DetailView):
    model = Product

上記の記述だけでhttprequestのGETが成立してしまうことをまず整理したい。

まずDetailViewはViewクラスを継承したものである。だからViewをカスタムしたものがDetailViewであり、GETのリクエストに対しViewのような流れでDetailViewも処理されると考えられる。そこでViewではどのようにGETのリクエストを返すか見てみる。

class ProductView(View):
    def get(self, request, *args, **kwargs):
        pk = self.kwargs["pk"]
        object = Product.objects.get(id=pk)
        context = {}
        context["object"] = object
        
        return render(request, "products/detail.html", context)

ProductDetailViewをProductViewと比べると、以下の違いがある。

  • objectを指定しないでもobjectが自動的に取得される。
  • 取得したobjectがcontextに自動で登録されている。
  • 自動的にオブジェクトをテンプレートにレンダリングされる。

細かく見ればさらに出てくるだろうが、まずこの3つが大きな違いだろう。
これらの違いはいかにして実現されているか、よく観察してみたい。

objectを指定しないでもobjectが取得される

これはDetailViewのget_object()メソッドが実行されると、自動的に特定のオブジェクトを取得することになっている。
https://github.com/django/django/blob/2.1/django/views/generic/detail.py#L20

def get_object(self, queryset=None):
    if queryset is None:
        queryset = self.get_queryset()

    pk = self.kwargs.get(self.pk_url_kwarg)
    slug = self.kwargs.get(self.slug_url_kwarg)
    if pk is not None:
        queryset = queryset.filter(pk=pk)
    ...
    try:
        # Get the single item from the filtered queryset
        obj = queryset.get()
    except queryset.model.DoesNotExist:
    ...
    return obj

DetailViewはget_objectメソッドを持つSingleObjectMixinを継承しているので、上記の機能を有する。話がそれるが、obj = queryset.get()はgetの引数がない。これについては下記の豆知識に詳細を書いておいた。

取得したobjectがcontextに自動で登録されている

これはBaseDetailViewのgetメソッドを見れば自動的に登録されていることが分かる。

BaseDetailView(SingleObjectMixin, View)のgetメソッド

def get(self, request, *args, **kwargs):
    self.object = self.get_object()      # 上記で説明したget_objectメソッドの実行
    context = self.get_context_data(object=self.object) # contextにobjectをぶち込んでいる
    return self.render_to_response(context)
    

ソース:https://github.com/django/django/blob/2.1/django/views/generic/detail.py#L105

そもそもユーザが任意のURLをリクエストすると、URLconf(=urls.py)でas_viewメソッドが実行される。as_viewメソッドはdispatchメソッドを呼び出す。そしてdispatchメソッドはhandlerという形式でViewまたはDetailViewのgetやpostメソッドを呼び起こす。この時にViewクラスはgetメソッドが存在していないので結果的にデベロッパーがgetメソッドを作らされるのである。それは先のないプラレールの線路の行き先を作るが如く...。一方DetailViewはgetメソッドが準備してあるのでデベロッパーが作る必要がない。この辺の背景を理解していると良いと思う。

自動的にオブジェクトをテンプレートにレンダリングされる

 render()関数はレンダリングしたHttpResponseオブジェクトを返す。
https://docs.djangoproject.com/ja/2.2/intro/tutorial03/#a-shortcut-render
render_to_responseメソッドはresponse_classを呼び出す。response_classはデフォルトではTemplateResponseオブジェクトを作成する。このオブジェクトはHttpResponse同じ場面で使うことができる(TemplateResponse ≒ HttpResponse)
この辺をもうちょっと資料を集めておきます。
https://docs.djangoproject.com/ja/2.2/ref/template-response/#using-templateresponse-and-simpletemplateresponse

豆知識

querysetからオブジェクトを取り出すとき、get()を使うが引数は必ずしも必要ない。通常はquerysetにオブジェクトが複数存在している時queryset.get(id=pk)みたいに記述する。しかしquerysetにオブジェクトが一個しか入っていない場合は例外として引数不要でget()を使うことができる。これは、ソースコードで使われている。
https://github.com/django/django/blob/2.1/django/views/generic/detail.py#L52

If you expect a queryset to return one row, you can use get() without any arguments to return the object for that row:

ドキュメント:https://docs.djangoproject.com/ja/2.2/ref/models/querysets/#django.db.models.query.QuerySet.get

toLocalStringメソッド

ベース

Javascriptメモ

本題

toLocalStringメソッド

対象オブジェクトを文字列化する。文字列化するのには他にtoStringメソッドもあるらしい。 これらの違いは、現在のロケールに応じた文字列表現を返しますかどうか。JSとUSだと文字列表現が異なる場合もありえるってこと。 pythonでは関数str()にあたるものが、toString、toLocalStringと整理して頭に入れておく。

メソッドの小文字と大文字に使い分ける規則はあるのか?

メソッドに前置詞にあたるものは小文字始まりになるという仮説を立てる。

Dateオブジェクト

DateオブジェクトにもtoLacaleStringメソッドがあった。
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Date/toLocaleString

配列

ベース記事

Javascriptメモ

種類

疑問点

配列って何か?連想配列って何?なんのためにある概念?それができると何ができるようになるの?

それができると何ができるようになるの?

配列と連想配列を使うと、複数のデータ集合を扱うことができる。

配列って何か?

配列は、要素にインデックス番号でアクセスすることができるようになる。

連想配列って何?

連想配列は、要素にキー(名前)でアクセスすることができるようになる

 

おそらくこの概念はpythonで言うlist型のデータや辞書型のデータの扱いについて関連するものかもしれない。

配列の扱い方

配列インスタンスの生成方法

ドキュメント:https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array#Syntax

配列のインスタンスの生成方法はpythonのように[]を使う方法と、Arrayオブジェクトに引数を入れて生成する方法の2通りがある。

var moji = ["A", "B", "C", "D" ]; #pythonのような書き方

var moji = new Array("A", "B", "C", "D");

配列の要素取り出し

>|js| console.log(lights[0]) ||<

pythonと同じ。で、文末にセミコロンを入れるとエラーが出てしまう。要素の取り出しの際はセミコロンは不要。要素の取り出しは文として扱うなということか??

 

連想配列の操作方法

pythonの辞書型データの定義とほぼ同じ。違うところは定義時に必要となるvarと文末に必要となる;に加えてキーを””でくくらなくてよいというところだけ。

UnorderedObjectListWarning: Pagination may yield inconsistent results with an unordered object_list

以下の注意メッセージが表示される

UnorderedObjectListWarning: Pagination may yield inconsistent results with an unordered object_list:******

解消方法

参考にしたところ:https://stackoverflow.com/questions/44033670/python-django-rest-framework-unorderedobjectlistwarning/44036414
自分の場合はdrfを利用せずにこのメッセージが出てきた。このエラーはインスタンスのリストの並び順が固定されいなく、フワフワしているためページネーションでうまくオブジェクトを表示することができないということらしい。
だからオブジェクトリストを特定のルールの並び順にして作成すれば良いとのことだった。実際に自分はProduct.objects.filter(category__startswith=c)としていた。

 

object_list = Product.objects.filter(category__startswith=c).order_by("-id")

これで注意メッセージが出なくなった。

django:adminページでインスタンスを検索する

adminでインスタンスの検索が必要になるケース

例えば運用しているアプリケーションのインスタンスに誤りがあるとする。このインスタンスを修正するためにadminから修正することになっていたとする。
この場合アドミンからインスタンスを選択し、修正を行う。しかしインスタンスが20,000件ほどあったらどうだろうか?検索できないとページを繰りながら該当インスタンスを探すことになる。200ページくらいページを繰りたいか?答えは否。

 

インスタンスを検索するフォームをアドミンに実装する

document:https://docs.djangoproject.com/ja/2.2/ref/contrib/admin/#django.contrib.admin.ModelAdmin.search_fields

検索フォームの実装はModelAdminを継承したクラスの属性にsearch_fieldsを定めてやるだけで終了する。

from django.contrib import admin
from products.models import Product


class ProductModelAdmin(admin.ModelAdmin):

	search_fields = ["productId"]
	


admin.site.register(Product, ProductModelAdmin)

このように書けば、新たにフォームがアドミンページに出現する。そしてproductidを打ち込むと、該当するインスタンスのみが抽出されるようになる。

この検索フォームの実装は前提条件がある。それは検索対象がCharFieldまたはTextFieldでことだ。
上記のフィールドではないフィールドを使って検索したい場合もある。その際は、クラス属性のlist_filterを使うと検索が可能になる。BooleanField、CharField、DateField、DateTimeField、IntegerField、ForeignKey、ManyToManyFieldであれば利用することができる。参考:https://docs.djangoproject.com/ja/2.2/ref/contrib/admin/#django.contrib.admin.ModelAdmin.list_filter

デプロイ時にプロジェクトに必要なライブラリを漏れなくインストールする

結論:pip freezeを使う

サーバーに必要なライブラリをインストールするために開発環境でpip freezeまたはpip listを使い確認しては、サーバーでライブラリをインストールしてきたが一気にインストールしてしまったほうが良い。

pip freeze > requirements.txt

これでテキストファイルができるので、サーバにて以下を実行すれば一気にライブラリをインストールできる。

pip install -r requirements.txt

django:PCとスマホで表示を変えたい

ユーザーのデバイスによって表示を変更したい

表示方法の変更としてレスポンシブデザインを採用しBootstrapを利用してきた。しかしレスポンシブな方法では納得いく表現ができなかった。
バイスによって異なるテンプレートを表示させることができるか調査して実装できれば実装してみたい。ユーザーのデバイスはuser-agentsを利用して捕捉することができるようだ。https://pypi.org/project/user-agents/
そのほかにdjango-user-agentsなるものもあった。これも同じことが実現できそうか?https://pypi.org/project/django-user-agents/
そもそも標準ライブラリのplatformを使えば対処できるのでは?という疑問も出てくる。何か分かったら更新する。

バイスによってテンプレートを変える

django-user-agentsを使うと手軽にテンプレートをデバイスによって使い分けられるようになった。テンプレートの使い分けはviews.pyでユーザのデバイスの種類を特定し、デバイスの種類によってrenderでテンプレートを使い分ける。

from django_user_agents.utils import get_user_agent

class ProductListView(View):

    def get(self, request, *args, **kwargs):
        object_list = Product.objects.all()
        context["object_list"] = object_list
        
        user_agent = get_user_agent(request)
        if user_agent.is_mobile:
            return render(request, "products/list_mobile.html", context)
            
        elif user_agent.is_tablet:
            return render(request, "products/list_tablet.html", context)
            
        else:
            return render(request, "products/list_pc.html", context)

django-user-agentsを使うとミドルウェアを通してrequestに属性を付け加えられる。これを利用してユーザーのデバイスを特定することができる。

インストール方法

pip install pyyaml ua-parser user-agents
pip install django-user-agents

settings.pyの設定項目

INSTALLED_APPS = (
    # Other apps...
    'django_user_agents',
)

# キャッシュを使うとdjango-user-agentsを素早く働かせることができる
CACHES = {
    'default': {
        'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
        'LOCATION': '127.0.0.1:11211',
    }
}


# キャッシュを使わない場合はNoneと記述。キャッシュを使う場合は基本的には'default'を使う
USER_AGENTS_CACHE = 'default'

# ミドルウェアにも下記を加える
MIDDLEWARE_CLASSES = (
    # other middlewares...
    'django_user_agents.middleware.UserAgentMiddleware',
)

django signalの情報を集める

signalの仕組みを理解するには2つのキーワードがある。senderとreceiverである。名前の通り、senderはsignalを送り出す役割を担う。receiverはそのsignalを受取り、何か挙動を起こす役割を持つ。receiverは関数またはインスタンスメソッドで無くてはならない。
senderとreceiverの両者はsignal dispatcherによってつながる。このsignal dispatcherってのはSignalのインスタンスである。

postgresのパスワードが分からない場合

postgresのパスワードが分からない場合

いろんな方のブログを見ていると、postgresqlをインストールすると同時にpostgresユーザーが作成される。そしてpostgresのパスワードも設定することになる、と書いてあるが、自分の場合postgresのパスワードを定める機会がなかった。パスワードを定める機会がなかったり、パスワードを忘れてしまった場合にはデータベースに接続できなくなる。このときの対処法を記録しておく。

手順

まずデータベース接続に無条件で接続できるように認証設定ファイルpg_hba.confを編集してpostgresqlに接続する。接続後、postgresのパスワードを再設定する。

無条件で接続するには, local all all trustに変更し、postgresqlを再起動すればよい。postgresqlがあるローカル環境ならパスワードなしで接続できるようになる。

次にpostgresのパスワード再設定だけれど、これはpostgresに接続した状態で以下のコマンドを実行すれば良い。

ALTER ROLE postgres WITH PASSWORD '新しいパスワード';

結果にALTER ROLEと表示されれば再設定できた。
このあとは再びpg_hba.confでlocal all all md5に戻してpostgresql(postgresql-11)を再起動してあげれば良い。

Bootstrapを見る

ドキュメント

https://getbootstrap.jp/docs/4.2/layout/grid/

レスポンシブレイアウト

レスポンシブレイアウトを構築するための5つの階層が定義されている。(extra small,small,medium,large,exyra large)
バイスを問わず、同じグリッドの場合に使いたいとき、class="col", class="col-*"を使う。

知りたいこと

カード横の空白を無くす方法

django:sendgridでメール本文において&が&amp;に変換されてしまう件について

状況

Checkクラスにはurlを格納する属性があり、一定時間ごとに各インスタンスurlを利用し、ウェブサイトにアクセスする。そして特定の情報が存在した場合にsendgridを使って自分のメールに送信する仕組みを設けた。メール本文にはアクセスしたurlを載せる。メール本文の作成は大まかに以下のようになっている。

from django.core.mail import send_mail
from django.template.loader import render_to_string

class CheckView(View):
    def get(self, request, *args, **kwargs):
    
        ・・・
        check_objs = Check.objects.all()
        for obj in check_objs:
            url = obj.url
            
        ・・・
        
            subject = "チェック結果"
            context = { "url" : url}
            message = render_to_string("mytemplate.txt", context)
        
            send_mail(
                subject,
                message,
                ["mynamesasasasa@gmail.com",],
                fail_silently=False,
                )
        ・・・

この状況でurlの&部分がすべて& amp;に変換されてしまった。

この問題の対処

この原因がどこによるモノなのか分からなかった。sendgridの諸設定によるものかもしれないし、djangoによるものなのかもしれない。
いろいろ試した結果、メール本文にはテンプレートと変数のレンダリングする方式ではなく、そのまま文字列(url)を送る方法を採用した。これにより&の&amp;変換問題が解消された。
コードは以下のような感じになる。

from django.core.mail import send_mail
from django.template.loader import render_to_string

class CheckView(View):
    def get(self, request, *args, **kwargs):
    
        ・・・
        check_objs = Check.objects.all()
        for obj in check_objs:
            url = obj.url
            
        ・・・
        
            subject = "チェック結果"
            # context = { "url" : url} ←削除
            message = url
        
            send_mail(
                subject,
                message,
                ["mynamesasasasa@gmail.com",],
                fail_silently=False,
                )
        ・・・

この結果sendgridから送ったメールがそのまま送ることができたので、原因はdjango側にある可能性が高いと推定した。

今回の原因と周辺情報

https://docs.djangoproject.com/ja/2.2/ref/templates/language/#automatic-html-escaping

要点にまとめると、djangoにはクロスサイトスクリプティング対策がされており、テンプレートレンダリングする場合には変数タグの出力を自動的にエスケープする仕様になっている。つまり常時エスケープ機能がonになっているので、今回の件で別案の解決方法としてエスケープ機能をオフにするときだけ設定する方法もあった。
エスケープ機能については具体的にはエスケープのオフにはエスケープオフの範囲によってタグを使い分ける。
変数単位でエスケープする場合、テンプレートまたはテンプレートの部分にエスケープする場合の2パターンがある。 前者は{{ data | safe }}とすればエスケープ機能がオフになる。後者はテンプレート内で{% autoescape off %}と{% endautoescape %}を挟んだ範囲が機能オフになる。

クロスサイトスクリプティングやセキュリティについて理解を深めたいならば、『体系的に学ぶ 安全なWebアプリケーションの作り方 第2版 脆弱性が生まれる原理と対策の実践』を読むと良いだろう。

python:自作モジュールのインポートについて

自作モジュール内でインポートをするのに困ったので解決方法をメモしておく。

そもそもimportするには?

ディレクトリ以下にあるファイルをインポートすることができる。

    MY_DIR----- myscript.py
           L--- myconfig.py
           L--- init.py

カレントディレクトリがMY_DIRにある時、myscript.py,myconfig.py,init.py内の変数や関数をfrom myscript import var みたいにインポートできる。カレントディレクトリ以下にないpyファイルをインポートする場合はインポートするパスを追加することで対応できる。

具体的には

パスに追加する方法はsys.path.append(パス)で追加できる。

     +-----B_DIR------ function.py
     |            L--- settings.py
    A_DIR
     |
     +--- MY_DIR----- myscript.py
           L--- myconfig.py
           L--- init.py

例えばfunction.pyの関数をmyscript.py内に呼び出す際には

myscript.pyにて
    
    import os, sys
    sys.sppend(os.path.join(os.path.dirname(os.path.dirname(__file__)),"B_DIR"))
    from function import func

こうすればB_DIRのパスを追加できるのでインポートすることができる。