タグ「Python」の 記事 9 件中 1 ~ 9 件を表示しています。

2 年前も最有力として採用した

仕事で詳細設計を書く段取りになりそうなので Markdown 記法ができるドキュメント生成用のパーサは今何があるのか再度調べてみた。 Sphinx の方も Markdown 対応が進んでいるようだが、やはりまだ 2 年前と同様に MkDocs が最有力のようだ。 ということで再度調べ直したが、やっぱり便利だ。 というより 2 年前に書いた自分の記事が参考になった。 自分で調べたことでも半年以上経つとほとんど覚えていない……。

build したファイルをローカルに置いて正しく見る設定

2 年前に調べた時に複数の Markdown ファイルをリンクする構成の場合 index.html へのリンクがうまく張られないという問題があった。 今もそうなのかと思って調べたところ、どうも 2 年前に考察したこんなことをしなくても mkdocs.yml への設定一発でできるようになっているようだった。 というより当時からできていて私が知らなかっただけなのかもしれない。

mkdocs.yml に以下のようにかく:

use_directory_urls: false

これは stamemo 様の MkDocs で生成したサイトをローカルで開くと index.html が開かれない問題という記事が非常に参考になった。 この設定を false にしておくと例えば hoge.md に対して HTML リンクが /hoge のようになってしまいディレクトリ配下の index.html を探索するサーバの挙動を期待するようなリンクの張られ方にならなくなる。 mkdocs パッケージのバージョンにもよるらしいが、私が今試したバージョンだと hoge.html としてビルドされて hoge.html へのリンクが張られた。

今だったらこのような mkdocs.yml にする

site_name: 'サイトネーム'
site_description: 'サイト説明'
site_author: '所有者名'
site_url: 'https://hoge.fuga.com/'
copyright: '著作表記'
use_directory_urls: false

theme:
  name: 'material'
  language: 'ja'
  palette:
    primary: 'cyan'
    accent: 'orange'
  font:
    text: 'BIZ UDPGothic'
    code: 'Consolas'
  features:
    - tabs

plugins:
  - search:
      lang: ja

markdown_extensions:
  - admonition
  - footnotes
  - codehilite:
      linenums: true

各 index.html へのリンクが不親切

MkDocs は以前記事で触れた通りとても便利な Markdown で文書を生成するツールなのだが、MkDocs で複数の Markdown ドキュメントをリンクする形 ([xxx](yyy) 形式での .md ファイル間のリンク) にするとリンクが意図通り作成されない。 具体的には <a href="hoge/index.html"> のようになってほしいところが <a href="hoge/"> のように生成されてしまう。 生成された HTML ドキュメントを Web サーバ上に配置した場合は hoge/ 形式でも正しく表示されるのだが、ローカルに置いた形でブラウザを起動してみる (つまり file:///C:/Users/fuga/hoge/... のような形式の URL) だと例えば hoge/ を開くとその直下の index.html が暗黙的に呼び出されるのではなく hoge/ ディレクトリ以下のファイル一覧が表示されてしまう。

この挙動に関しては MkDocs でマテリアルデザインな Markdown ドキュメントサイトを作ろうにも以下のように書いてある:

Webサーバに載せることが前提
>MkDocsで生成した静的サイト用ファイルは、ローカルで index.html を開く使い方は想定されていません。 開くことと表示することはできますが、リンクから他の記事に遷移することができません。 これはMarkdownファイルへのリンクが ./TheTitle/ のように張られており、ファイルプロトコル上では これが ./TheTitle/index.html と解釈されず、正しく表示できません。

理屈はわかるが、HTML ドキュメントを生成したからといって Web サーバを立てて使うとは限らない。 何とかローカルで見たい。

index.html を正しく補ってやる

というわけで対象となるリンクを正しく置き換える Python スクリプトを書いた:

import glob, os, re

# MkDocs がビルドした HTML が置かれているディレクトリ
SITE_PATH = 'C:/Users/fuga/hoge/site'

# ルート index.html と各フォルダに分かれている index.html を対象とする
files = glob.glob(os.path.join(SITE_PATH, 'index.html')) + glob.glob(os.path.join(SITE_PATH, '*', 'index.html'))

for file in files:
    # HTML 読込
    with open(file, 'r', encoding='utf-8') as fp:
        html = fp.read()

    # index.html を付与
    html2 = re.sub(r'href="(.*?)/"', 'href="\\1/index.html"', html)

    # . と .. というリンクもあるのでそれも index.html を付加する...
    html3 = html2.replace('href=".."', 'href="../index.html"').replace('href="."', 'html="./index.html"')

    # HTML 書込
    with open(file, 'w', encoding='utf-8') as fp2:
        fp2.write(html3)

これを適当な名前で保存 (例えば mkdocs_converter.py) し、ソース内の SITE_PATH を MkDocs の site ディレクトリに書き換え python3 mkdocs_converter のように実行すれば変換される。

2020/07/30 追記: 設定一発で解決できる

実は設定一発で解決できた。 拙記事を参照されたい。

オリバー
2020-06-25 18:28:31
すごく困っていたのですが、このスクリプトのおかげですごーく助かりました。ありがとうございました。
コジオン
2020-06-25 19:30:28
おお、お役に立てて良かったです。こんな記事でも書いておくものですね。

久々に使い勝手の良さに感動した

ドキュメンテーションツールといえば Sphinx が定番だったし過去私もよく使用していたのだが Sphinx が標準でサポートしているマークアップ言語である reStructuredText がお世辞にも書きやすいとは言えず常に記法を調べたり別の箇所からコピペするなどして頑張って書いていた記憶がある。 最近では技術系に関わらず文書を起こす場合は Markdown がデファクトの地位を確立していると思う。 Sphinx でも設定すれば Markdown を使えるようだがこれも使い勝手がいいとはいえない。 設定が簡単で Markdown を書くことに集中できるようなツールを探していたのだが、今回見つけた MkDocs がとても素晴らしかったので自分用メモを兼ねてここに書き記しておく。

  • Markdown なのに最初から表組みの拡張が入っている (しかも書きやすい)
  • mkdocs serve でローカルサーバが起動するが Markdown ドキュメントを編集すると即時反映される (ビルド不要)
  • Sphinx の admonition だったり footnotes (注釈) が設定 1 行付け足すだけで簡単に使用できるようになるし他の機能も必要に応じて 1 行書くだけで即追加可能

導入・設定

導入に関してはカンタンにドキュメントが作れるmkdocsをはじめてみようがとても分かりやすかった。 基本は Python をインストールして pip からすべて導入できるので楽だ。 細かい設定に関しては MkDocsによるドキュメント作成が詳しい。

その他の設定は MkDocs 公式 (英語) を確認すればよい。 また使用するテーマに関してだが readthedocs か material のどちらかがいいということだが私は material にした。 CSS をいじらずに配色 (マテリアルデザインにおけるテーマ色とアクセント色) が簡単に変更できるし、日本語対応がされている。 後何故か readthedocs の方だとコードハイライト時にうまく表示されなかった。

私が導入した設定

site_name: 'サイトネーム'
site_description: 'サイト説明'
site_author: '所有者名'
site_url: 'サイト URL'
copyright: '著作表記'

theme:
  name: 'material'
  language: 'ja'
  palette:
    primary: 'light blue'
    accent: 'pink'
  font:
    text: 'UD デジタル 教科書体 NK-R'
    code: 'Consolas'
    
extra:
  search:
    language: 'jp'
    
markdown_extensions:
  - admonition
  - codehilite
  - footnotes
  - pymdownx.inlinehilite
  - pymdownx.tasklist:
      custom_checkbox: true

Material for MkDocs

Material for MkDocs 公式 (英語) にすべて網羅されているので設定はここを見れば良い。 言語ロケールの選択をすると全体的に日本語表記になる (検索も日本語対応されている) し、テーマ色やフォント (コードハイライトと別々に定義できる) の設定も可能だ。

Extensions

以下を導入した:

  • Admonition (Sphinx のような警告文)
  • CodeHilite (コードハイライト)
  • Footnotes (文末注釈)
  • pymdownx.inlinehilite (インラインコードのハイライト)
  • pymdownx.tasklist (チェックリスト)

やりたいこと

この Blog システムでは毎日データベースのダンプファイルを出力してバックアップを取っているのだが、あくまでサーバー内へのバックアップ出力なのである日突然サーバーがダウンして再起動できなくなってしまった場合サルベージできなくなってしまう。 バックアップファイルは外部ストレージに保存したいところだ。 今までは適当な期間をおいて Google Drive に手動でバックアップを移していたのだが、私は毎日 Blog に日記をつけているのでこれだとバックアップデータが十分に古い場合が出てくる。 毎日バックアップファイルを出力した時点で Google Drive に自動で転送したい。

PyDrive

Qiita に素晴らしい記事があったので大体の部分はこれでできた。 一部ハマった部分があったのでここに注釈として記しておく。

settings.yaml

公式サイトにも書いてあるが settings.yaml という設定ファイルを Python 実行ファイルと同じディレクトリに置く必要がある。 Google Developers Console から出力した client_secrets.json でも良いが、これだと認証情報が保存されない。毎回ブラウザベースでの認証が必要になってしまう。 具体的には YAML 内の以下の部分である:

save_credentials: True  # 認証情報を保存する
save_credentials_backend: file  # 何に保存するか. 今のところ 'file' しか指定できないらしい
save_credentials_file: credentials.json  # 認証情報保存ファイル名

上記の設定がされていることで初回アクセス時 (credentials.json がまだない時) はブラウザベースの認証画面がキックされ、認証が成功すると credentials.json として保存される。

また settings.yaml 内に OAuth の対象を絞り込むための設定があり、サンプルで以下のようになっている:

oauth_scope:
  - https://www.googleapis.com/auth/drive.file
  - https://www.googleapis.com/auth/drive.install

これだとファイルと作成のみの許可となっておりディレクトリなどの検索ができずにハマる (特にエラーなど出力されずヒットしないだけとなる)。 公開するアプリなどではない場合、特に権限を絞り込む必要がない場合は以下のように全権限にしておけば問題ない:

oauth_scope:
  - https://www.googleapis.com/auth/drive

ディレクトリに対し操作を行う場合必ず ID が必要

例えばローカルディレクトリ /path/to に対し操作を行う場合はシステムに対し一意となっているディレクトリパス /path/to の部分が分かっていれば良いのだが Google Drive の場合はそれだと駄目で必ずディレクトリに対する ID を指定する必要がある。 この ID はどうやって調べるのかというところだが、Web 版の Google Drive を使用している場合に対象となるディレクトリに移動して URL を見てみると https://drive.google.com/drive/folders/0B3GHspmhxAvDOXRCGTlSWjhrRWs のようになっている。 この 0B3GHspmhxAvDOXRCGTlSWjhrRWs がそのディレクトリの ID であり、これを使用して以下のように操作できる:

google_auth = GoogleAuth()
google_auth.CommandLineAuth()
drive = GoogleDrive(google_auth)

# 対象の ID のディレクトリ配下のファイル (ゴミ箱に入っていないもの) を全取得
file_list = drive.ListFile({'q': "'{}' in parents and trashed=false".format('0B3GHspmhxAvDOXRCGTlSWjhrRWs')}).GetList()

# Google Drive 上の対象 ID のディレクトリ内に hoge.txt を作成
file = drive.CreateFile({'title': 'hoge.txt', 'parents': [{'id': '0B3GHspmhxAvDOXRCGTlSWjhrRWs'}]})

この 'q': '(ID) in parents and trashed=false' というクエリが奇妙に映るが、これは Google Drive APIs (REST) の記法なので仕方ないところのようだ。

対象ディレクトリのファイルを全削除した上でローカルからコピーするサンプル

ということで Django のコマンドラインで動作するように以下のように実装してみた:

class Command(BaseCommand):
    help = 'Google Drive にバックアップファイルを転送する.'

    def add_arguments(self, parser):
        parser.add_argument('path', type=str, help='バックアップ元ファイルパス')
        parser.add_argument('parent_id', type=str, help='Google Drive 転送先ファイルパス')

    def handle(self, *args, **options):
        self.stdout.write(self.style.SUCCESS("Google Drive 転送処理を開始します."))

        # Google Drive 認証を行う
        google_auth = GoogleAuth()
        google_auth.CommandLineAuth()
        drive = GoogleDrive(google_auth)

        # まず旧ファイルを削除
        file_list = drive.ListFile({'q': "'{}' in parents and trashed=false".format(options['parent_id'])}).GetList()
        for file in file_list:
            file.Delete()
        self.stdout.write(self.style.SUCCESS("{} 内の旧ファイルを削除しました.".format(options['parent_id'])))

        # backup フォルダ配下のファイルを軒並み転送
        for directory_path, directory_names, file_names in os.walk(options['path']):
            for file_name in file_names:
                file = drive.CreateFile({'title': file_name, 'parents': [{'id': options['parent_id']}]})
                file.SetContentFile(os.path.join(options['path'], file_name))
                file.Upload()
                self.stdout.write(self.style.SUCCESS("{} を転送しました.".format(file_name)))

        self.stdout.write(self.style.SUCCESS("Google Drive 転送処理が完了しました."))

第一引数に転送元ローカルディレクトリを指定、第二引数に Google Drive の対象ディレクトリ ID を指定して以下のように実行できる:

manage.py sendtodrive backup/ 0B3GHFpmhx9vdUFFLVPE4EG9YNnM

Professional は高い

私は Python は仕事で使用しているわけではないのだが、趣味のプログラミング、特に今はこの Blog を開発する言語として使用している。 Python を日常的に書く方はエディタや IDE は何を使用しているのだろうか。 Vim で書いているという方も多いと思うが、私が愛用しているのは PyCharm である。

PyCharm の元となっている IntelliJ IDEA は言語ごとにそれぞれのエディションがあり、私が仕事でよく使用しているのが PHP 向けの IDE である PhpStorm である。 この PhpStorm を使う前は Eclipse の PHP プラグインなど使用していたと思うが、この PhpStorm が便利すぎて今となってはこれがないと仕事に支障が出るし、これなしで PHP を書きたくないと思うほどだ。

PhpStorm には Community 版などというものはないし何より重要な仕事道具の為特に疑問も持たず毎年お金を払うようにしている。 しかしこれがちょっと使うだけの場合は悩んでしまうくらい高い。 初年度 89 ドル、次年度 71 ドル、3 年目以降 53 ドルとなっている。 そして PyCharm のサブスクリプション金額も同様である。

ただ PyCharm の方には上記の通りのお金がかかる Professional と無料版の Community というのがある。 Web アプリなど関係なく Python を学習したいというだけであれば Community 版で十分だろう。 私は Web アプリ開発で使用しているので Professional 版が好ましいのだが何となくお金が惜しいのでしばらく Community 版で作業してみたがいろいろと辛い場面はあった。 以下それを列挙しようと思う。

Django サポートがない

Professional には Django のサポートがあるのだがこれがすごく便利で、画面右上の再生アイコンから Django のテストサーバーの起動・停止ができるしデバッグ実行もできてしまう。 PHP の場合ブレークポイントで止めるには Xdebug の設定など必要で面倒 (なので大抵は var_dump などで変数を出力してデバッグする) なのだが PyCharm ならば何もしなくても普通にブレークポイント張ってデバッグ実行すればちゃんとその位置で実行が止まる。 Community にはこれがないのでテストサーバ起動はコンソールを叩く (manage.py runserver)。

一番作業効率に差が出そうだと思ったのは Django テンプレートのサポートで、Django テンプレートの記法 {{ variable }}{% for x in xs %}などを書いているところでもコード補完が利く。 コード補完が効かないと適宜フレームワークのドキュメントを眺めながら書いていかなければならず非効率である。

Web 系のコードがすべて補完が効かない

PyCharm の Community には Web development の機能がない。 これは TypeScript や LESS, SASS などのモダンな言語だけでなく JavaScript, CSS, HTML の補完も効かないことを意味する。 これも前述と同様で、補完が効かない場合適宜ネットで検索して必要なフィールド、メソッドを探してから書かなければならない場面が多く辛い。

ただ、この 1 点だけであれば PhpStorm を既に持っているならばそこだけ PhpStorm の手を借りるという手もあるが、それも少し面倒な話だ。

IDE 上からデプロイができない

PhpStorm もそうだが Professional には SFTP などのプロトコルでリモートサーバにデプロイする機能がある。 同期したくないファイルを除外するルールなども書けるし、本番反映したい場合は IDE 上からメニューを選択するだけでいいので便利だ。

Community はこれがないので WinSCP を入れて自分で必要なファイルを選択して同期するか Cygwin 上で rsync コマンドを叩く必要がある。

データベースサポートがない

DB のサポートがないので Community でデータベースを見る場合はやはりコンソールでコマンドを叩いて見ることになる。

「とりあえずできる」と「快適にできる」は大きな差

別に Community であったとしても工夫次第で Django を使用した Web アプリの開発は可能なのだが「困ったらコンソールでコマンドを叩け」ではそもそも何故 IDE を使用しているのか、という本質を見誤りそうになる。 仕事で使用するのであれば迷いもなく購入するのだが、こういうわけで PyCharm に関しては未だに迷い続けている。 仕事も Python になってくれれば一件落着なのだが……と今でも少し思う。

あと Android Studio が無償になっているのは間違いなく IntelliJ 系 IDE の普及に貢献できていると思う。 私などは Android Studio から入り PhpStorm を試して止められなくなったクチだ。

NT
2018-05-05 16:23:08
こちらの記事を読まさせていただきました。とても参考になりました。プロフェッショナル版を購入しようかどうか考えているところだったのですごく助かりました。
ただ、値上がりしたのか今は1年目200ドル〜3年目120ドルとかなり割高になっています!趣味では手を出せない値段ですね。。。
コジオン
2018-05-05 18:26:15
コメント頂きましてありがとうございます。値段に関してですが、仰っているのは企業向けアカウント (BUSINESSES AND ORGANIZATIONS) ではないでしょうか?個人開発向け (INDIVIDUAL CUSTOMERS) でしたら初年度 89 ドル、3 年目 53 ドルです。まぁ、ちょっと高いことには変わりませんが……。

ちなみにですが、私は JetBrains 日本公式販売代理店である WillBrains (https://www.willbrains.jp/) で IntelliJ IDEA Ultimate を購入しました。代理店経由だと少しだけ安く買えるようです。
らりるれお
2019-12-26 18:20:59
自分のがprofessional版かcommunity版か確認する方法を教えてください
コジオン
2019-12-26 19:30:38
ヘルプのバージョン情報で表示されます。
というより、アプリのショートカットを調べれば表示されると思います。

描画が遅くなってきたのでページングを実装した

当 Blog のタイトル一覧ギャラリーであるが、最初のうちは別に件数も少なかったので常に全件表示する感じでも全くストレスを感じなかった。 だがこの Blog は毎日欠かさず何かしら書いているため最近になると記事の件数は 600 件を超え、画像に関しては 300 件近くになってしまった。 その為さすがに全件表示してしまうと描画にそこそこ時間がかかるようになってきた。 そこで Web アプリでよくあるページング処理を実装してみたわけだが、大抵の Web フレームワークには Pagination の実装が内蔵されているが当 Blog で使用している Django にもあったので割と簡単に実装することができた。

データ取得処理の差し替え

例えば変更前のデータ取得コードが以下のようになっているとする:

context['posts'] = Post.objects.filter(**params)

これを Paginator オブジェクトでラップして必要なページを取得すれば良い:

paginator = Paginator(Post.objects.filter(**params), 100)  # 1 ページ 100 件とする
number_of_page = request.GET.get('page') if request.GET.get('page') else 1  # GET パラメータで来たページインデックス
context['posts'] = paginator.page(number_of_page)

これだけで GET パラメータで page=2 などと渡せば正しく 101 件目から 200 件目を描画してくれる。

ページング UI

後はページングの為の UI を描画する処理をテンプレートに書く。 公式ドキュメントにも書いてあるが PaginatorPage オブジェクトにはそれぞれページングの UI を描画するために使える値が含まれている。

paginator.count  # 検索対象総件数 (例えば 732 件中 101 から 200 件を表示している場合の「732」)
paginator.num_pages  # 総ページ数 (例えば 732 件で 100 件ずつの表示ならば「8」)
paginator.page_range  # [1, 2, 3, 4, 5, 6, 7, 8] などといった全ページ番号のリスト. これをテンプレート側で for 文で回して使う

page.has_next()  # 次ページがあるか
page.has_previous()  # 前ページがあるか
page.has_other_pages()  # 他のページがあるか (使うのか?)
page.next_page_number()  # 次ページ番号
page.previous_page_number()  # 前ページ番号
page.start_index()  # 開始件数 (例えば 732 件中 101 から 200 件を表示している場合の「101」)
page.end_index()  # 終了件数 (例えば 732 件中 101 から 200 件を表示している場合の「200」)

page.number  # ページ番号
page.paginator  # Page インスタンスから Paginator インスタンスを取得する. なので Paginator を Template 側に渡す必要はない

これを使って当 Blog では以下のようにページングを実装してみた (posts は View 側から渡された Page インスタンスである):

<div class="pagination-count">
    {{ posts.paginator.count }} 件中 {{ posts.start_index }} ~ {{ posts.end_index }} 件を表示しています。
</div>
{% if posts.paginator.num_pages > 1 %}
<div class="pagination">
    {% for i in posts.paginator.page_range %}
        {% if i == posts.number %}
    <em>{{ i }}</em>
        {% else %}
    <a href="?page={{ i }}{% if tag_id %}&tag_id={{ tag_id }}{% endif %}">{{ i }}</a>
        {% endif %}
    {% endfor %}
</div>
{% endif %}

ページング程度であればすべて自分で実装することもそこまで大変ではないが、フレームワークで用意されているページングの実装を使うとやはり記述するコードが減って楽だ。

項目 22: 辞書やタプルで記録管理するよりもヘルパークラスを使う

長いけど要するに mutable な辞書や要素を位置で指定するタプルを複雑な情報保持に使用するのは止めてクラスを使おうという事だろう。 でも namedtuple は知らなかった。クラスを作るほどでもないけど辞書や tuple だと心もとない場合に使えそうだ。

項目 23: 単純なインタフェースにはクラスの代わりに関数を使う

他の言語だと、フックが抽象クラスで定義されます。

Java の事を言っているのだろう。まぁ Python を始めとした昨今の LL 言語は大抵関数をファーストクラスオブジェクトとして関数の引数に直接渡すことができるが、

状態を保守するために関数が必要な場合、状態を持つクロージャを定義する代わりに、__call__ メソッドを提供するクラスを定義することを考える。

なるほど。クロージャを使っていたかもしれない。

項目 24: @classmethod ポリモルフィズムを使ってオブジェクトをジェネリックに構築する

内容が難しくて若干辛みが……。ともかく、Python はクラスに対して __init__ メソッドという 1 つのコンストラクタしかサポートしていないので、代わりのコンストラクタを定義するために @classmethod を使うこと。

項目 25: 親クラスを super を使って初期化する

親クラスのコンストラクタを単純に (親クラス名).__init__() で呼び出すと、特にダイヤモンド継承時に親のコンストラクタが不当に 2 回呼びだされてしまい意図しない動作となることが書かれている。こういう場合は組み込み関数 super() を使うとダイヤモンド継承の頂点の __init__ は 1 回しか呼び出されない。

項目 26: 多重継承は mix-in ユーティリティだけに使う

まぁ多重継承は宜しくないのであまり使わないが mix-in 的要素だったらアリということだろう。

項目 27: プライベート属性よりはパブリック属性が好ましい

Python のプライベート変数 (頭に __ をつける) は厳密には特殊な構文で普通にアクセスできてしまう。

なぜ、プライベート属性の構文は、厳密な可視性を強制しないのでしょうか。最も単純な回答は、よく引用される Python のモットー「みんないい大人なんだから。」です。

Python はそういうところがある言語なのは認識している。定数が無くて UPPER_SNAKE_CASE で書いた変数を定数をみなすところとか。

プライベート属性は、コントロール外のサブクラスによる名前衝突を避けるためだけに使用する。

それが Python 流ということか。プロテクテッドの方がまだマシというのが驚いた。

項目 28: カスタムコンテナ型は collections.abc を継承する

abc は Abstract Base Class (抽象基底クラス) だ。Python でもこれを使えば抽象クラスが使えるということ。

項目 14: None を返すよりは例外を選ぶ

このあたりは Java と同じだ。null を返すより例外を選ぶ。

項目 15: クロージャが変数スコープとどう関わるかを知っておく

外側の関数の変数が内側の関数のスコープ内で参照できるのは知っていたが、外側の関数の変数への代入の挙動は知らなかった。 コードを見てみるのが早い:

def sort_priority(values: list, group: set):
    def helper(x: int):
        return 0 if x in group else 1, x  # group が helper 関数内で参照できる
    values.sort(key=helper)

numbers = [8, 3, 1, 2, 5, 4, 7, 6]
sort_priority(numbers, {2, 3, 5, 7})
print(numbers)  # [2, 3, 5, 7, 1, 4, 6, 8]

が、以下は正しく動かない:

def sort_priority2(values: list, group: set):
    found = False
    def helper(x: int):
        if x in group:
            found = True  # 外側の関数の変数に代入しているように見えるが, 実際は新たなローカル変数の定義, 代入
            return 0, x
        return 1, x
    values.sort(key=helper)
    return found

numbers = [8, 3, 1, 2, 5, 4, 7, 6]
found = sort_priority2(numbers, {2, 3, 5, 7})
print(found)  # False (想定どおりではない)
print(numbers)  # [2, 3, 5, 7, 1, 4, 6, 8] (これは合っている)

こういう場合 Python 3 だと nonlocal を使うらしい。上記コードの def helper(x: int): の下に nonlocal found とすれば動く。 ただ何か global 変数定義と似た危うさを感じるのでなるべく使わないほうがいいように見える。

ちなみに PyCharm だと nonlocal を使う前のコードで外側のローカル変数名と同じだと警告を出してくれる。

項目 16: リストを返さずにジェネレータを返すことを考える

その通りとしか言いようがないが、内包表記からジェネレータに変えるのは []() に替えるだけなので楽だが、 普通の関数の場合はなかなか思考が働かなかったりはする。

項目 17: 引数に対してイテレータを使うときには確実さを尊ぶ

いきなり内容が難しくなった気がする。イテレータは結果を一度しか生成しないので以下の様なことが起きる:

def f():
    for x in range(10):
        yield x

r = f()  # イテレータを変数に代入
print(list(r))  # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
print(list(r))  # [] になる. 既にイテレートは終了してしまっているがエラーは発生しない

print(list(f())  # 常に新たなイテレータを使うようにすれば OK
print(list(f())  # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

クラスで __iter()__ を定義したものを使った場合は挙動が違うのか。知らなかった:

class A(object):
    def __iter__(self):
        for x in range(10):
            yield x

r = A()  # インスタンスを変数に代入
print(list(r))  # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
print(list(r))  # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

内部的には list() が新たなイテレータオブジェクトを作成するために A.__iter__() を呼び出すのだということが書いてある。 ただ、これだと複数回イテレートしてしまうことになるので、そうしたくない場合は結果を変数に代入して再利用するのがよい。

項目 18: 可変長位置引数を使って、見た目をすっきりさせる

これは Java にもある機能ではある。特筆すべきことはない:

def f(x, *args):
    return x + ': ' + ', '.join(args)

print(f('hoge', 'fuga', 'hage'))  # hoge: fuga, hage

項目 19: キーワード引数にオプションの振る舞いを与える

Python のキーワード引数は非常に有用だと思う。これのお陰で可読性が高まる:

def location(name, lat=35, lng=139, bearing=0):
    return '{}: ({}, {}) / {}'.format(name, lat, lng, bearing)

print(location(name='家', lat=35.1829, lng=139.8237))  # 引数の意味が明確なので読みやすい
print(location('家', 35.1829, 139.8237))  # これでも呼べるがどれが何の引数なのかがパット見わからない

特に関数定義が離れていた場合引数の位置と意味を調べるのが非常に面倒なので、キーワード引数にすることにより意味が明確になり読みやすい。

項目 20: 動的なデフォルト引数を指定するときには None とドキュメンテーション文字列を使う

関数の引数のデフォルト値が mutable なインスタンスだった場合に値が関数の初回呼び出し時の 1 回しか評価されないので 2 回目以降の呼び出しで前回の値が残っており奇妙な動作になってしまう、というのは Python では有名な話だと思う:

def log(message, when=datetime.now()):
    print('{}: {}'.format(when, message))

log('hoge')  # 2016-02-06 09:04:39.305342: hoge
log('fuga')  # 2016-02-06 09:04:39.305342: fuga

現在時刻をロギングするはずが、常に最初の log() 呼び出しの日時が出力されてしまう。 これを避けるためにデフォルト引数に None を使用しドキュメンテーション文字列に文書化せよとある:

def log(message, when=None):
    """タイムスタンプを使用したログメッセージを出力する.

    :param message: 出力するメッセージ
    :param when: メッセージ出力時の datetime. デフォルトは現在時刻
    """

    when = datetime.now() if when is None else when
    print('{}: {}'.format(when, message))

log('hoge')  # 2016-02-06 09:09:36.333739: hoge
log('fuga')  # 2016-02-06 09:09:36.333805: fuga

項目 21: キーワード専用引数で明確さを高める

Python 3 にキーワード引数を要求する構文があるとは知らなかった。普通の引数とキーワード引数の間に *, を挟むというもの。これは使いたい:

def log(message, *, when=None):
    when = datetime.now() if when is None else when
    print('{}: {}'.format(when, message))

log('hoge')
log('fuga', datetime.now())  # キーワード引数を使用していないので TypeError
log('fuga', when=datetime.now())  # OK

Python 2 にはこの構文が無いので **kwargs を使用せよとの事。

Effective Python の発売日だった

自宅に Effective Python がやってきた。233 ページしか無いのに 3,200 円もするという技術本。 まぁ技術本なんてこのような値段設定は珍しくないが、やっぱり高い。

オライリーの書籍は O'Reilly Ebook で電子書籍としても購入することができるが、こちらは PDF フォーマットな上に若干出版にタイムラグがある。いつもだったら Ebook を待つのだがちょっと今回はすぐ読んでみたいのとたまには物理的な紙の媒体で読んでみたいというので前もって楽天で予約しておいたというわけだ。

折角高いお金を出して買ったので、ちょっと一人読書会でもやってみよう、というお話。 Blog や Qiita などで不特定多数に向けて何かを書くことを心掛けると、いい加減なことを書けないのでちゃんと理解しようという縛りを自分に設けることができる。

Effective Python の目次は O'Reilly の書籍紹介ページにある。 各項目に対して自分なりの考察若しくは感想文を書こうというわけだ。というわけで、以下 1 章 Python 流思考 (Pythonic Thinking) に関する一人読書会を実行する。

項目 1: 使っている Python のバージョンを知っておく

Python には 2.x 系と 3.x 系があり双方には互換性が無い。処理系にも CPython, Jython, IronPython, PyPy などあるよ、という話。 OS X や CentOS 等に最初からインストールされている Python は未だ 2.x 系である。Ubuntu は 16.04 LTS から Python 3.5 がデフォルトになるようだ。

これから新規 Python プロジェクトを立ち上げる場合は特に理由がない場合は 3.x 系を使用すること。 とはいえ、仕事としてやっているとどうしても 2.x の方を相手せざるを得ないのも事実。

項目 2: PEP 8 スタイルガイドに従う

Python プログラマの従うべき最も有名なコーディング規約として PEP 8 がある。 PyCharm などの IDE であれば最初から PEP 8 のチェックが入るし Vim や Emacs のようなエディタでもチェックする方法がある。

書籍には PEP 8 の中でも特筆すべき項目について列挙されている。筆者が気になったものを以下に引用する。

各行は、長さが 79 文字かそれ以下とする。

PEP 8 のこれはかなり有名なのだが、何故 79 文字以下なのだろうか。80 文字では駄目だったのだろうか。

またコードが短めの Python ならまだこれも守れないこともないが Java で 80 文字制限などしたら悲惨なことになるし、 今のディスプレイは高精細なので割と横に多く表示できるので 120 文字くらいでもいい気はしないでもないが、まぁこういう規約なので守っておく。

長さを使って空値かどうかをチェックしない。空値が暗黙に False と評価されることを使う。

暗黙型変換を使うのは危ないのでは?長さを使ったほうが安全では?と思ってしまった。 特に PHP では if ($hoge) と書くと if ($hoge == true) の意味 (緩やかな比較) となり PHP では割と変な値まで true, false になってしまうというのがあるので strlen($hoge) と書くのは結構よくやるので Python もアリではないかと思っていたが、暗黙型変換を用いた方が構文がシンプルになって良いということだろうか。

# 冗長な書き方
if len(somelist) == 0:
    ...

# 好ましい書き方
if not some list:

項目 3: bytes, str, unicode の違いを知っておく

Python 2 では文字列は str で Unicode 文字列を扱うのに unicode を使わなければならなかった。 英語圏の人は全く困らない仕様だが、我々の使用しているような非 ASCII 文字を使用している言語の場合 u'日本語' などと頭に u を付けて Unicode 文字列であることを明示しなければならなかった。

しかし Python 3 ではこれば str に一本化され単純に '日本語' と表現できるようになった。つまり Python 2 の unicode が Python 3 の str になったという話。

一時期 Unicode と UTF-8 がごっちゃになっていた時があったのだが UTF-8 はあくまで Unicode の効率的なエンコード方式であり別物である。 Unicode についてがすごく分かりやすかった。

Unicode 文字をバイナリ (生の 8 ビット値) で表すには多くの手法があります。一番多いのは UTF-8 符号化です。 重要なのは Python 3 の str インスタンスと Python 2 の unicode インスタンスがバイナリ符号化を伴っていないことです。 Unicode 文字をバイナリデータに変換するには、メソッド encode を使わなければなりません。

つまりファイルから読み込んだ場合などで bytes 型になっている時はエンコードされている状態 (多くは UTF-8) なのでそれを decode しなければならない。

項目 4: 複雑な式の代わりにヘルパー関数を書く

複雑な式を 1 行に詰め込むなとか部分的に共通化できるならヘルパー関数を書けという話。 Python は気軽に関数内関数が書けるので、このあたりは積極的に使っていきたいところ。

項目 5: シーケンスをどのようにスライスするか知っておく

Python は文字列もシーケンス型なので配列のようなスライスが簡単に使えるのが便利で美しい。

リストの先頭からスライスするときには、添字のゼロは省いて、見た目をスッキリさせましょう。

assert a[:5] == a[0:5]

0 ... つけてしまっていたかもしれない。

末尾までスライスするときには、末尾の添字は冗長なので省きましょう。

assert a[5:] == a[5:len(a)]

これはちゃんとできていた。Java 等の substring が第二引数を付けないと末尾までスライスするという意味なので類推しやすかったように思う。

一箇所、パット見よくわからなかった箇所が以下:

添字 start も end もないスライスに代入を行うと、(新しいリストが作成されるのではなくて) リストの内容全体が右辺のリストが参照している要素に置き換わります。

a = []
b = a
print('Before', a)  # []
a[:] = [101, 102, 103]
assert a is b  # True
print('After ', a)  # [101, 102, 103]

なるほど単純に a = [101, 102, 103] とやってしまうと a is not b になってしまう。リストの参照を変えずにリストの内容全体を書き換えたい時に使うわけだ。

項目 6: 1 つのスライスでは start, end, stride を使わない

start, end, stride とは somelist[start:end:stride] みたいなものの事で stride でリストの取得間隔を指定できるが、 これが読みにくいのでなるべく避けましょうという事だった。 それ以前にあまり使うことが無いわけだが……。

項目 7: map や filter の代わりにリスト内包表記を使う

これはもうその通りとしか言いようが無い。リスト内包表記は便利すぎる。他の言語にも欲しいくらいだ。

項目 8: リスト内包表記には 3 つ以上の式を避ける

リスト内包表記は for 文をネストできるが、当然だがやり過ぎると読みにくいので普通に for 文を使うほうが良い。

項目 9: 大きな内包表記にはジェネレータ式を考える

これもその通りとしか言いようがない。Python はリスト内包表記を少し書き換えるだけでジェネレータ式になるので便利だ。

項目 10: range よりは enumerate にする

恥ずかしながら私も range() で書きがちだったのでこれは肝に命じることにする。

for i in range(len(flavor_list)):
    flavor = flavor_list[i]
    ...

よりも

for i, flavor in enumerate(flavor_list):
    ...

の方がずっと簡潔だという話。PHP の foreach, Java の拡張 for 文にあたるものは Python では enumerate として用意されていると覚える。

項目 11: イテレータを並列に処理するには zip を使う

これも Python の便利なところで、他の言語でも zip() が欲しいと思い自分で実装してしまったケースもあるくらいだ。

項目 12: for と while ループの後の else ブロックは使うのを避ける

恥ずかしながら私はこの else が通る場合の条件を理解していなかった:

for i in range(3):
    ...
else:
    ...  # for 分が break されなかった場合に呼ばれる (!!)

Python での else, except, finally のすべての用法から、初めてのプログラマは for/else の else 部分は「ループが完了しなかったらこれをしなさい」という意味だと思い込むものです。

for 文が実行されなかった時 (対象のリストが 0 件だった時) だと思っていた。全然違った。 というわけで、確かに混乱の元なので使わないほうがいいだろう。わかりやすさを好む Python でこんな分かりにくいパーツがあるのが驚いた。

項目 13: try/except/else/finally の各ブロックを活用する

こちらでは前章と違い else も活用せよ と書いてある。しかし、こちらも誤解を呼ぶ (正直パット見分からない) から使わないほうが良いのではないかと思うが……。