「PythonユーザのためのJupyter実践入門」を写経しながら読みました

最近、Python機械学習やデータサイエンス方面で使う機会がありました。

これまでPythonをWebアプリで使うことが多かったこともあり、最初のうちは各種ツールを雰囲気で使っていました。

ただ、使っていくうちに、ツールを基本的なところから理解したくなりました。

そこで、知りたいツールが網羅されている「PythonユーザのためのJupyter実践入門」を写経しつつ読みました。

PythonユーザのためのJupyter[実践]入門

PythonユーザのためのJupyter[実践]入門

 

感想

本書では、自分の知りたかった

  • Anaconda
  • Jupyter Notebook
  • Pandas
  • Matplotlib

が基本的なところから書かれていました。

そのため、雰囲気で使っていた

  • condaコマンドで仮想環境を作る方法
  • Jupyter Notebookのメニューやボタンの意味
  • Matplotlibの日本語文字化けを解消する方法
  • Matplotlibで図を描画する方法
    • フィギュアとサブプロットの概念(p140)

などがおさえられました。

 
特に良かったのが、Matplotlibの書き方について

  • MatplotlibにはMATLAB-styleとOOP-Styleの2つの書き方がある
  • 本書ではOOP-Styleの書き方を推奨する

と書かれていたことです(p253)。

今までMatplotlibにはいろいろな関数があって分かりづらいと感じていたことが、それが実はスタイルが2つあるせいだと分かりました。

そのため、今後は「Webにあるサンプルはどちらで書かれているか?」が判断でき、悩むことなくサンプルを読み進められそうでした。

 
また、本書の推奨するOOP-Styleであれば、どの図形を描くときも

# フィギュアを用意する
fig = plt.figure()

# サブプロットを用意する
ax = fig.add_subplot(111)

# 図形をプロット
ax.<描画メソッド>

# タイトルやラベルなどをつける
ax.set_title('foo')

# 描画
plt.show()

という流れで書いていけそうでした。

 
3章以降は写経しながら読み進めましたが、進めるうちにコード補完が欲しくなりました。そのため、PyCharm + Jupyter Notebookに切り替えました。
Using IPython/Jupyter Notebook with PyCharm - Help | PyCharm

見栄えは劣るものの、日頃PyCharmを使っていることもあり、切り替えて以降はスムーズに写経できました。

 
一方、Pandasについてはまだ理解しきれていないと感じたため、本書以外の書籍や自分好みのデータを探して、引き続き手を動かしていこうと思います。

 
最後になりましたが、著者のみなさま、良い本をありがとうございました。

Jupyter Notebookで「TypeError: __init__() got an unexpected keyword argument 'io_loop'」エラーのため、コードが実行できない

$ jupyter notebook でJupyter Notebookを起動したところ、

  • コードを入力し、Ctrl + Enter を押してもコードが実行されない
  • Jupyter Notebookの右側に connected が青点滅している

ことがありました。

 
その時の対応をメモします。

 
目次

 

環境

  • Mac OSX 10.11.6
  • Anaconda 3.5.1
    • pyenvでインストール

 

調査

Jupyter Notebookを起動したターミナルを見ると、以下のエラーが繰り返し出ていました。

[I 10:04:58.541 NotebookApp] Adapting to protocol v5.1 for kernel b8c41504-eb3c-4aa2-b879-c650ba061127
[E 10:04:58.542 NotebookApp] Uncaught exception in /api/kernels/b8c41504-eb3c-4aa2-b879-c650ba061127/channels
    Traceback (most recent call last):
      File "/Users/foo/.pyenv/versions/anaconda3-5.1.0/envs/jupyterbook-env/lib/python3.6/site-packages/tornado/websocket.py", line 498, in _run_callback
        result = callback(*args, **kwargs)
      File "/Users/foo/.pyenv/versions/anaconda3-5.1.0/envs/jupyterbook-env/lib/python3.6/site-packages/notebook/services/kernels/handlers.py", line 262, in open
        super(ZMQChannelsHandler, self).open()
      File "/Users/foo/.pyenv/versions/anaconda3-5.1.0/envs/jupyterbook-env/lib/python3.6/site-packages/notebook/base/zmqhandlers.py", line 176, in open
        self.send_ping, self.ping_interval, io_loop=loop,
    TypeError: __init__() got an unexpected keyword argument 'io_loop'

 
このエラーを調べたところ、GitHubのissueに情報がありました。
Unable to start Jupyter · Issue #3202 · jupyter/notebook

For now, you'll need to downgrade to tornado 4.x. We'll be making a new notebook release soon that avoids this error.

https://github.com/jupyter/notebook/issues/3202#issuecomment-357199274

とのことです。

 
そのため、 conda list で手元のtornadoのバージョンを調べてみました。

$ conda list
# packages in environment at /Users/foo/.pyenv/versions/anaconda3-5.1.0/envs/jupyterbook-env:
#
# Name                    Version                   Build  Channel
...
tornado                   5.0.2            py36h1de35cc_0  
...

tornadoは5系でした。

 

対応

tornadoを4系の最新版にします。
https://pypi.org/project/tornado/#history

$ conda install tornado=4.5.3

 
Jupyter Notebookを起動します。

$ jupyter notebook

 
コマンドを実行してみると、問題なく動作しました。ターミナルにもエラーが出ていません。

jQuery1.6.0より、属性がない時のattr()の戻り値が、空文字からundefinedへと変更されていた

jQueryのバージョンを上げる時に出くわしたのでメモを残します。

 
目次

 

環境

 

現象

jQueryattr() を使って属性の値を取得しているコードのうち、

<p>Hello World</p>

<script>
    // class属性がないのに attr() を使っている
    result = $('p').attr('class');
</script>

と、属性が定義されていない場合、後続の処理で予期しない挙動をしていました*1

 

原因

jQuery1.6.0より、 attr() の戻り値が変わっていました。

属性が定義されていない場合の attr() の戻り値が

  • 〜1.5.2:空文字
  • 1.6.0〜:undefined

へと変わっていました。

 
公式ドキュメントの attr() でも undefined が返ってくるとの記載がありました。
.attr() | jQuery API Documentation

 
これより、元のソースコードは空文字が返ってくると想定していたようなので、undefinedが返ってきて想定外の挙動になったようでした。

 
jQuery1.6〜では、 attr()prop() に関する変更も入っているので、合わせてこのあたりも修正されたのでしょうか。
jqueryのattrとprop(とdataとvalとhtmlとかつまり属性とかの取得)の使い分け一覧 - Qiita

 

実験

ためしに

result = $('p').attr('class');
console.log(result);
console.log(typeof result);

を試してみたところ、以下の結果となりました。

 
jQuery1.5.2

f:id:thinkAmi:20180703123106p:plain

 
jQuery1.6.0

f:id:thinkAmi:20180703123123p:plain

 

ソースコード

GitHubに上げました。 attr_undefined ディレクトリの中が今回のファイルです。
https://github.com/thinkAmi-sandbox/jQuery-misc

*1:クラス名をattr()で取得するのが良いのかは、ここでは置いておきます

DjangoのModelのFieldのオプション null と blank の違いについて

DjangoにてDBのNOT NULL 制約を外したい場合、nullとblankのどちらを使えばよいのか忘れることがあるため、メモしておきます。

 
目次

 

環境

 

違い

stackoverflowにありました。
python - differentiate null=True, blank=True in django - Stack Overflow

 
公式ドキュメントの場合はこのあたり。
https://docs.djangoproject.com/en/2.0/ref/models/fields/#null

  • null オプションは、NOT NULL 制約を外す
    • 基本、何も値を与えなければNULLをセット
    • ただし、CharField型などの文字型については、NULLではなく空文字をセット
      • このため、 blank=True かつ unique=True の場合は、 null=True にしないと空文字が複数回登場することになり、エラーとなる
    • BooleanFieldでNULLを扱いたい場合は、 NullBooleanField にする
  • blank オプションは、保存時に何もデータを渡さなくても、バリデーションでOKとする (値の任意入力化)

でした。

 

確認

stackoverflowではPostgreSQLMySQLで試していたので、今回はSQLiteで試してみます。

Model

今回は

  • CharField
  • IntegerField
  • DateTimeField
  • BooleanField
  • NullBooleanField

なModelを用意します。

class NullBlank(models.Model):
    charNull = models.CharField(max_length=10, null=True)
    charBlank = models.CharField(max_length=10, blank=True)
    charNullBlank = models.CharField(max_length=10, null=True, blank=True)
    charBlankUnique = models.CharField(
        max_length=10, blank=True, unique=True,
    )
    charNullBlankUnique = models.CharField(
        max_length=10, null=True, blank=True, unique=True,
    )

    intNull = models.IntegerField(null=True)
    intBlank = models.IntegerField(blank=True)
    intNullBlank = models.IntegerField(null=True, blank=True)

    dateNull = models.DateTimeField(null=True)
    dateBlank = models.DateTimeField(blank=True)
    dateNullBlank = models.DateTimeField(null=True, blank=True)

    boolNull = models.BooleanField(null=True)
    boolNullBlank = models.BooleanField(null=True, blank=True)
    boolBlank = models.BooleanField(blank=True)

    nullboolNull = models.NullBooleanField(null=True)
    nullboolBlank = models.NullBooleanField(blank=True)
    nullboolNullBlank = models.NullBooleanField(null=True, blank=True)

 

マイグレーションファイルの作成

BooleanFieldがNULLを許可しないため、マイグレーションファイル作成時にエラーとなりました。

$ python manage.py makemigrations nullblank
nullblank.NullBlank.boolNull: (fields.E110) BooleanFields do not accept null values.
  HINT: Use a NullBooleanField instead.
nullblank.NullBlank.boolNullBlank: (fields.E110) BooleanFields do not accept null values.
  HINT: Use a NullBooleanField instead.

 
それらのFieldをコメントアウトして、再度実行すると、マイグレーションファイルができました。

$ python manage.py makemigrations nullblank
Migrations for 'nullblank':
  nullblank/migrations/0001_initial.py
    - Create model NullBlank

 

sqlmigrateで、発行されるSQLを確認

CREATE TABLEのところを整形して見やすくしてみます。

$ python manage.py sqlmigrate nullblank 0001
BEGIN;
--
-- Create model NullBlank
--
CREATE TABLE "nullblank_nullblank" (
  "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, 
  "charNull" varchar(10) NULL, 
  "charBlank" varchar(10) NOT NULL, 
  "charNullBlank" varchar(10) NULL, 
  "charBlankUnique" varchar(10) NOT NULL UNIQUE, 
  "charNullBlankUnique" varchar(10) NULL UNIQUE, 
  "intNull" integer NULL, 
  "intBlank" integer NOT NULL, 
  "intNullBlank" integer NULL, 
  "dateNull" datetime NULL, 
  "dateBlank" datetime NOT NULL, 
  "dateNullBlank" datetime NULL, 
  "boolBlank" bool NOT NULL, 
  "nullboolNull" bool NULL, 
  "nullboolBlank" bool NULL, 
  "nullboolNullBlank" bool NULL
);
COMMIT;

 
いい感じです。

 

マイグレーションファイルを適用
$ python manage.py migrate nullblank
Operations to perform:
  Apply all migrations: nullblank
Running migrations:
  Applying nullblank.0001_initial... OK

 

データの投入

実際にデータを投入してみます。

以下のようなDjangoコマンドを用意します。

# management/commands/null_blank.py

from django.core.management import BaseCommand
from django.utils import timezone

from nullblank.models import NullBlank

class Command(BaseCommand):
    def handle(self, *args, **options):
        NullBlank().save()

 
投入してみます。

$ python manage.py null_blank

sqlite3.IntegrityError:
NOT NULL constraint failed: nullblank_nullblank.intBlank
NOT NULL constraint failed: nullblank_nullblank.dateBlank
NOT NULL constraint failed: nullblank_nullblank.boolBlank

blank=True なカラムがエラーとなりました。

 
そのため、

NullBlank(
    intBlank=0,
    dateBlank=timezone.now(),
    boolBlank=True,
).save()

とすると、登録できました。

 
しかし、もう一度実行すると、

$ python manage.py null_blank

sqlite3.IntegrityError:
  UNIQUE constraint failed: nullblank_nullblank.charBlankUnique

と出ました。

UNIQUE制約のある列で、空文字が重複したようです。

 
そのため、最終的には以下となります。

NullBlank(
    intBlank=0,
    dateBlank=timezone.now(),
    boolBlank=True,
    charBlankUnique=timezone.now().strftime('%Y/%m/%d %H:%M:%S')
).save()

 

SQLiteCLIでデータを確認

SQLiteCLIを使い、実際のデータベースの中身を見てみます。
https://www.sqlite.org/cli.html

手元のMacではsqlite3コマンドがインストール済なので、それを利用します。

# SQLite3ファイルがある場所へと移動
$ ls -a db.sqlite3
db.sqlite3

# データベースファイルに対し、CLIを起動
$ sqlite3 db.sqlite3 
SQLite version 3.16.2 2017-01-06 16:32:41
Enter ".help" for usage hints.

# ヘッダを付けて表示
sqlite> .header on

# カラムモードで表示
sqlite> .mode column

# NULLの場合は、"NULL" という文字を出力
# こうしないと、空文字とNULLの違いが分からないため
sqlite> .nullvalue NULL

# CharField関係
sqlite> select id, charNull, charBlank, charNullBlank, charBlankUnique, charNullBlankUnique from nullblank_nullblank;
id          charNull    charBlank   charNullBlank  charBlankUnique  charNullBlankUnique
----------  ----------  ----------  -------------  ---------------  -------------------
1           NULL                    NULL                            NULL               
2           NULL                    NULL           2018/06/20 08:4  NULL  

# IntegerField関係
sqlite> select intNull, intBlank, intNullBlank from nullblank_nullblank;
intNull     intBlank    intNullBlank
----------  ----------  ------------
NULL        0           NULL        
NULL        0           NULL  


# DatetimeField関係
sqlite> select dateNull, dateBlank, dateNullBlank from nullblank_nullblank;
dateNull    dateBlank                   dateNullBlank
----------  --------------------------  -------------
NULL        2018-06-20 08:39:24.738832  NULL         
NULL        2018-06-20 08:48:50.256008  NULL 


# BooleanField関係
sqlite> select boolBlank, nullboolNull, nullboolBlank, nullboolNullBlank from nullblank_nullblank;
boolBlank   nullboolNull  nullboolBlank  nullboolNullBlank
----------  ------------  -------------  -----------------
1           NULL          NULL           NULL             
1           NULL          NULL           NULL   

 

ソースコード

GitHubに上げました。 nullblank アプリケーション以下が、今回のファイルになります。
https://github.com/thinkAmi-sandbox/Django20-sample

Django2.0のプロジェクトのurls.pyにおける、include()での引数namespaceについて調べてみた

Django 2.0にて、プロジェクトの urls.py

from django.urls import path, include

urlpatterns = [
    path('old/', include('myapp.urls', namespace='old')),
    ...
]

アプリの urls.py

from django.urls import path
from django.views.generic import TemplateView

urlpatterns = [
    path('namespace/',
         TemplateView.as_view(template_name='myapp/url_with_include.html'),
         name='without_app'),
]

としたところ、以下のエラーが発生しました。

'Specifying a namespace in include() without providing an app_name '
django.core.exceptions.ImproperlyConfigured: Specifying a namespace in include() without providing an app_name is not supported. 

Set the app_name attribute in the included module, or pass a 2-tuple containing the list of patterns and app_name instead.

 
この時に調査・対応したことをメモしておきます。

 
目次

 

環境

 

調査

エラーメッセージより、

  • includeで呼ばれるモジュールに、 app_name を設定
  • (URLパターン, 名前空間) のタプルをinclude()に渡す

のどちらかを実装すれば良さそうでした。

 
また、 django.urls.include() の公式ドキュメントを見たところ、引数 namespace を使いたい場合の記載もありました。
include() | django.urls functions for use in URLconfs | Django documentation | Django

ただ、エラーメッセージで示されたどちらかの実装をしていないと、引数 namespace が使えないようです。

 
次に、いつから app_name が導入されたのかを調べてみたところ、 1.9のようでした。
https://docs.djangoproject.com/en/1.9/topics/http/urls/#url-namespaces-and-included-urlconfs

1.8以前は、include()の引数に namespace を使う方法が書かれていました。
https://docs.djangoproject.com/en/1.8/topics/http/urls/#url-namespaces-and-included-urlconfs

 
チュートリアルも、1.9から書き直されていました。

 
最後に、いつ頃からエラーが発生する用になったのかと思い、挙動を確認したところ、

でした。

Django 2.0のリリースノートにある

Support for setting a URL instance namespace without an application namespace is removed.

https://docs.djangoproject.com/en/2.0/releases/2.0/#features-removed-in-2-0

のあたりが関係していそうでした。

 
以上より、冒頭のソースコードはDjango2.0で1.8時代の実装をしていたために発生していたようでした。

 

対応

調査方法でも書きましたが、主な対応方法として以下の3つが考えられました。

  • プロジェクトのurls.pyのinclude()では namespace 引数を使用せず、アプリのurls.pyに app_name を追加
  • プロジェクトのurls.pyのinclude()の引数として、 (アプリのurls.py, 名前空間) となるタプルを渡す
  • プロジェクトのurls.pyのinclude()に namespace 引数を使用し、アプリのurls.pyでも app_name を追加

 
どの方法が良いのかと思い、Djangoの公式チュートリアルを見たところ、アプリのurls.pyに app_name を追加する書き方をしていました。
https://docs.djangoproject.com/ja/2.0/intro/tutorial03/#namespacing-url-names

そのため、特に制限がなければ、アプリのurls.pyに app_name を書くのが良いのかなと思いました。

 

動作確認

今回は、url テンプレートタグを使った時にいずれの方法でもURLリバースできるか にて確認してみます。

 

実装

まず、プロジェクトのurls.pyは

urlpatterns = [
    # アプリのurls.pyに app_name 設定があるパターン
    path('with/', include('myapp.urls_with_app_name')),

    # アプリのurls.pyには app_name 設定がなく、includeの引数をタプルにして名前空間を渡すパターン
    # (urlpatternsのあるモジュール, 名前空間)
    path('without/', include(('myapp.urls_without_app_name', 'without'))),

    # アプリのurls.pyに app_name 設定があるが、名前空間を別に用意するパターン
    path('over/', include('myapp.urls_overwrite_app_name', namespace='replaced')),
    # アプリのurls.pyの app_name 設定をそのまま使うパターン(再掲)
    path('not_over/', include('myapp.urls_overwrite_app_name')),
]

とします。

 
次に、app_nameのあるアプリのurls.pyはそれぞれ

myapp.urls_with_app_name.py

app_name = 'with'

# urlpatternsは、他も同じなので、以下省略
urlpatterns = [
    path('namespace/',
         TemplateView.as_view(template_name='myapp/url_with_include.html'),
         name='with_app'),
]

 
myapp.urls_overwrite_app_name.py

app_name = 'overwrite'

とします。

 
あとは、テンプレートを

<ul>
    <li><a href="{% url 'with:with_app' %}">app_name がある場合</a></li>
    <li><a href="{% url 'without:without_app' %}">app_name がない場合</a></li>
    <li><a href="{% url 'replaced:over_app' %}">app_name が上書きされた場合</a></li>
    <li><a href="{% url 'overwrite:over_app' %}">app_name がそのままの場合</a></li>
</ul>

として用意します。

 

動作確認

開発サーバを起動し、curlでアクセスしてみると、いずれの方法でもURLリバースできていました。

$ curl -L http://localhost:8000/with/namespace
<!DOCTYPE html>
...
<ul>
    <li><a href="/with/namespace/">app_name がある場合</a></li>
    <li><a href="/without/namespace/">app_name がない場合</a></li>
    <li><a href="/over/namespace/">app_name が上書きされた場合</a></li>
    <li><a href="/not_over/namespace/">app_name がそのままの場合</a></li>
</ul>

 

ソースコード

GitHubに上げました。
https://github.com/thinkAmi-sandbox/Django20-sample

Mac + Polipoで、SSHの動的フォワード(SOCKS) に HTTP Proxy をつなげてみた

よく分からないタイトルですが...

同一LAN内で、以下の論理構成とした時に、AndroidでアクセスしたときもRaspberry Piからのアクセスに見えるかどうかを試した時のメモです。

Android
|
(HTTP Proxy、ポート 23000)
|
Mac + polipo
|
(ssh -N -D 0.0.0.0:16000)
|
Raspberry Pi
|
Windows (Web Server)

 
物理的な構成は、

Android
|
(WiFi)
|
WiFiアクセスポイント
|
(有線LAN)
|
スイッチングハブ
|
(有線LAN)
|
Raspberry Pi, Windows, Mac

です。

 
目次

 

環境

  • Android 7.0系
    • 192.168.10.100
  • Mac OS X 10.11.6
    • LUA4-U3-AGTにて有線化
    • 192.168.10.99
    • Polipo 1.1.1
  • Raspberry Pi 2 Model B
    • Rasbpian
    • Macからssh可能
    • 192.168.10.201
  • Windows10
    • Python 3.6系
    • Webサーバは、Pythonpython -m http.server 9000 で実行中
    • 192.168.10.105

 
polipoを選んだ理由ですが、SOCKSをHTTPに変換できるProxyを探してみたらヒットした、というだけです。

ただ、今はメンテナンスも終了しているため、他に同じようなのがあれば、教えていただけるとありがたいです。
https://github.com/jech/polipo

 

準備

MacからRaspberry PiSSH可能にする

以前と同じようにセットアップします。
Raspberry Pi 2 Model B + docker-compose上に、Django + PostgreSQLなアプリをデプロイしてみた - メモ的な思考的な

 
内容としては

です。

 

Polipo

Macの場合、Homebrewでインストールします。

$ brew install polipo

 

Polipoの設定ファイルを作成

今回は

  • SOCKS5をHTTPにする、HTTP Proxyを作成
    • HTTP Proxyのポートは、 23000
  • キャッシュしない
  • 設定ファイルは ~/polipo_config/config として保存

とします。

# IPv4のみ接続可能
proxyAddress = "0.0.0.0"

# HTTP Proxyのポート
proxyPort = 23000

proxyName = "localhost"

# SOCKS Proxyの設定:SOCKSのポートは16000
socksParentProxy = "127.0.0.1:16000"
socksProxyType = socks5

# キャッシュをしない
maxDiskCacheEntrySize = 0 
diskCacheUnlinkTime = 0 

# 名前解決にシステムデフォルトのDNSを使う
dnsUseGethostbyname = true

# リダイレクトキャッシュをしない
dontCacheRedirects = true

# Cookieキャッシュをしない
dontCacheCookies = true

 
設定ファイルの詳細は、以下が参考になりました。

 

各機器の起動

Windows上のWebサーバ起動
python -m http.server 9000

 
Webサーバが動作しているか、Macからcurlでアクセスしてみます。

$ curl http://192.168.10.105:9000
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
...

 
Windowsコマンドプロンプト上にも、アクセスした時のMacIPアドレスが記録されました。

Serving HTTP on 0.0.0.0 port 9000 (http://0.0.0.0:9000/) ...
...
192.168.10.99 - - [12/Jun/2018 21:30:53] "GET / HTTP/1.1" 200 -

 
Androidもブラウザでアクセスしてみると、Windowsコマンドプロンプト上にログが残ります。

192.168.10.100 - - [12/Jun/2018 21:32:54] "GET / HTTP/1.1" 200 -
192.168.10.100 - - [12/Jun/2018 21:32:54] code 404, message File not found
192.168.10.100 - - [12/Jun/2018 21:32:54] "GET /favicon.ico HTTP/1.1" 404 -

 

MacからRaspberry PiSSH + 動的フォワード(SOCKS)

動的フォワード(SOCKS)となるように、SSHします。念のため v オプションを付けて進行状況が分かるようにしておきます。

ssh -N -D 0.0.0.0:16000 pi@192.168.10.201 -v

 
今回はパスワード認証のため、途中でパスワードを入力します。

pi@192.168.10.201's password: 
debug1: Authentication succeeded (password).

 

Mac上のPolipoを起動

SOCKSのポートにHTTP Proxyをつなぎ、ポート 23000 でHTTPアクセスを受け付けるようにします。

$ polipo -c ./polipo_config/config 
Established listening socket on port 23000.

なお、今回はPolipoを起動したときのみ、HTTP Proxyとして動作させるようにしました。停止は Ctrl + C です。

 

AndroidのProxy設定を変更

WiFi設定にて、Proxy設定を行います。

なお、手元のAndroidではHTTP Proxyしか存在しなかったので、実験に好都合です。

 
AndroidのProxy設定は、

  • プロキシ:手動
  • プロキシのホスト名:MacIPアドレス
    • 192.168.10.101
  • プロキシポート:polipoで設定したHTTP Proxyのポート
    • 23000

とします。

 
HTTP Proxyに変更後、AndroidのブラウザでWindows上のWebサーバへアクセスします。

コマンドプロンプトのログを見てみると、Raspberry Piのものになっていました。

192.168.10.201 - - [12/Jun/2018 21:49:44] "GET / HTTP/1.1" 200 -

 
期待通りの環境ができました。

Pythonで、super()で呼ばれる親メソッドの中で呼ばれるメソッドを、子でオーバーライドしてみた

良いタイトルが思い浮かばなかったのですが...

以下のソースコードを実行した時に、何がprintされるかを試した時のメモです。

class Parent:
    def reply(self):
        self.say()

    def say(self):
        print('parent!')


class Child1(Parent):
    def reply(self):
        super().reply()

    def say(self):
        print('child1!')


if __name__ == '__main__':
    c1 = Child1()
    c1.reply()
    # => ??

 
このソースコードの挙動です。

  • 子の reply() メソッドでは、親の reply() メソッドを呼ぶ
  • 親の reply() メソッドでは、 self.say() メソッドを呼ぶ

気になる点です。

  • 親子どちらの say() メソッドが呼ばれるのか?

 
目次

 

環境

 

Python3.6の場合

前述のコードを py3.py として保存し、実行してみます。

(env36) $ python py3.py 
child1!

子の say() メソッドが呼ばれました。

 
処理順を確認するため、printを仕込んでみます。

class Parent:
    def reply(self):
        print('[parent - reply]{}'.format(type(self)))
        self.say()

    def say(self):
        print('[parent - say  ]{}'.format(type(self)))
        print('parent!')

class Child1(Parent):
    def reply(self):
        print('[child  - reply]{}'.format(type(self)))
        super().reply()

    def say(self):
        print('[child  - say  ]{}'.format(type(self)))
        print('child1!')

if __name__ == '__main__':
    print('--- parent reply --->')
    p = Parent()
    p.reply()
    print('--- child1 reply --->')
    c1 = Child1()
    c1.reply()

 
実行してみます。

$ python py3.py 
--- parent reply --->
[parent - reply]<class '__main__.Parent'>
[parent - say  ]<class '__main__.Parent'>
parent!
--- child1 reply --->
[child  - reply]<class '__main__.Child1'>
[parent - reply]<class '__main__.Child1'>
[child  - say  ]<class '__main__.Child1'>
child1!

super()で呼んだ親クラスの reply() メソッドの引数selfに Child1 クラスのインスタンスが渡されています。

これは、Python3の super().reply()super(Child1, self).reply() と同じであり、後者が引数に self が渡されていることからも分かります。

その結果、Parentクラスで self.say() した時に、Child1クラスの say() メソッドが呼ばれます。

 
もし、super()を使わない場合は、親クラス.メソッド(引数としてselfを渡す) という形式、ここでは Parent.reply(self) とします。

class Child3(Parent):
    def reply(self):
        print('[child  - reply]{}'.format(type(self)))
        Parent.reply(self)

    def say(self):
        print('[child  - say  ]{}'.format(type(self)))
        print('child3!')

 
実行してみると、super()と同じ結果となりました。

[child  - reply]<class '__main__.Child3'>
[parent - reply]<class '__main__.Child3'>
[child  - say  ]<class '__main__.Child3'>
child3!

 

Python2.7の場合

Python3のコードのうち、以下のように差し替えます。

  • print文へと変更
  • 引数なしの super() がないため、 super(Child1, self) を使う

 

class Parent:
    def reply(self):
        print '[parent - reply]{}'.format(type(self))
        self.say()

    def say(self):
        print '[parent - say  ]{}'.format(type(self))
        print 'parent!'

class Child1(Parent):
    def reply(self):
        print '[child  - reply]{}'.format(type(self))
        super(Child1, self).reply()

    def say(self):
        print '[child  - say  ]{}'.format(type(self))
        print 'child1!'

if __name__ == '__main__':
    print('--- parent reply --->')
    p = Parent()
    p.reply()
    print('--- child1 reply --->')
    c1 = Child1()
    c1.reply()

 
実行してみます。

--- parent reply --->
[parent - reply]<type 'instance'>
[parent - say  ]<type 'instance'>
parent!
--- child1 reply --->
[child  - reply]<type 'instance'>
...
TypeError: super() argument 1 must be type, not classobj

エラーとなりました。super()のところで例外が起きてるようです。

原因は、Parentクラスの定義 class Parent: が、Python2の場合 old-style classes になるためです。

 
そのため、Python2でも動作させるためには、new-style classes として、Parentクラスで object を継承します。

class Parent(object):
    def reply(self):
        # あとは同じ

 
実行結果です。  

[child  - reply]<class '__main__.Child1'>
[parent - reply]<class '__main__.Child1'>
[child  - say  ]<class '__main__.Child1'>
child1!

new-style classesのため、 <class '__main__.Child1'> へと出力が変わりました。

 
もしくは、 old-sytle classesでも使える、 Parent.reply(self) にします。

class Child1(Parent):
    def reply(self):
        print '[child  - reply]{}'.format(type(self))
        Parent.reply(self)

 
実行結果です。

child  - reply]<type 'instance'>
[parent - reply]<type 'instance'>
[child  - say  ]<type 'instance'>
child1!

old-style classesのため、 <type 'instance'> と出力されています。

 

ソースコード

GitHubに上げました。
https://github.com/thinkAmi-sandbox/python_misc_samples/tree/master/e.g._call_overrided_method_using_super

 

参考