画像の特徴量を抽出するHOGについて

Pythonデータサイエンスハンドブックの最後、5.14節で、HOGを使って顔画像を検出している。
HOGが何なのかよく分かってなかったので、軽くメモ。

HOGの概要

HOGというのはHistogram of Oriented Gradientsの略だ。画像の特徴量を抽出する方法の1つである。
下記サイトによれば、実際に使う場合は、scikit-learn、もしくはOpenCVのライブラリが必要らしい。以下ではscikit-learnについて書く。
HOG特徴量 [いかたこのたこつぼ]

HOGの次元

Scikit-ImageのHOGの出力について - Qiita でも解説がされているが、この記事はちょっと情報が古い。

scikit-image公式ドキュメント内のAPI解説がこちら。
Module: feature — skimage v0.15.0 docs
関数の引数にfeature_vectorというパラメータがある。
defaultではTrueである。Trueだと関数の出力が一次元ベクトルになり、Falseだと出力は5次元の配列になる。
では5次元の配列の、各次元のサイズは何かということになるが、それにはセルとブロックという概念が必要になる。

画像からHOG特徴量の抽出 - Qiita
ある一定サイズのピクセルをセルと呼ぶ。デフォルトだと、1つのセルは8×8ピクセルである。
そしてセルを更にまとめたものをブロックと呼ぶ。デフォルトだと、1つのブロックは3×3セル、すなわち24×24ピクセルである。
(本当は図があれば一発でわかりやすいんだけど……上の記事にいい図があるので参照)

5次元の配列の、各次元のサイズは以下の通り。

  • 行方向の、ブロックの置き方の数
  • 列方向の、ブロックの置き方の数
  • ブロックの1行にあるセルの数 (デフォルトでは3)
  • ブロックの1列にあるセルの数 (デフォルトでは3)
  • 調べる方向の数(デフォルトでは9)

feature_vectorパラメータがTrueの場合には、出力配列のサイズは以上の5つの積になる。


「ブロックの置き方は互いに重なり合うから、特定のセルについての情報が複数回含まれているはずだけど、それで良いの?」がよく分からない。
あるセルの勾配の情報は1回含めれば十分な気がするんだけど。

ウェブサイトの色のコントラストがガイドラインを満たしているか確認する方法

概要

  • ウェブコンテンツの背景色と文字の色のコントラストには、ガイドラインが存在する
  • 基準を満たしているか確認するためには、各種ツールでチェックするとよい

ブログの記事を書いたので、アイキャッチ画像を作ろうとしていた。
最近、配色の色見本を買ってみたので、その中から色を選んで作ってみることにした。

ある配色の黄色い色がきれいだったので、これを文字の色にすることに決めた。
背景には青系の色を選んで……っと。

f:id:soratokimitonoaidani:20190327231446p:plain

おや。これはまずい気がする。ハレーションを起こしているので、チカチカして見づらい組み合わせになってしまった。

色の組み合わせの良し悪しはどうやって判断すれば良いんだろう。
……と思って調べてみたら、以下の記事を見つけた。この記事をもとにしらべてみた

Webサイト・スマホアプリの制作者がカラーとアクセシビリティについて知っておくべきこと | コリス

色のコントラストに関するWeb Content Accessibility Guidelines

背景色と文字色のコントラストについては、WCAGというものに記述されている。正式名称はWeb Content Accessibility Guidelinesで、「ウェブコンテンツをよりアクセシブルにするための広範囲に及ぶ推奨事項」である。
Web Content Accessibility Guidelines (WCAG) 2.0
これはガイドラインなので、これに反しているから罰則があるというわけではないが、見やすいウェブサイトを作るための指針としては良いだろう。
WCAGの1.4.3に「コントラスト(最低限)」、1.4.6に「コントラスト(高度)」という項目がある。

2つの色(背景色と文字色)を指定すると、コントラスト比という値を求めることができる。
コントラストは1~21の値で表される。
真っ白(#FFFFFF)と真っ黒(#000000)の場合がコントラスト比が最高となり、21である。2つの色が全く同じ場合は、コントラスト比が1である。絶対に判別できないので視認性が最悪である。
この極端な例から分かるように、コントラスト比が大きいほうが視認性が良くなる。で、その値が一定以上ならば良いらしい。

基準値は以下の通り。

- コントラスト(最低限) コントラスト(高度)
一般の場合 4.5 7
大きな文字の場合 3 4.5

大きな文字の場合は多少基準が緩くなるらしい。(「大きな文字」の定義や、その他の例外については割愛。元のガイドラインを参照)

最低限の基準と高度な基準があるというのは、成績の「可」「良」みたいな感じだろうか(あまりよく分かっていない)。
サイトを製作する上でどれくらい厳格にした方が良いか、を考えて選べばよいのだろう。

コントラストをチェックするcolor.review

Webサイト・スマホアプリの制作者がカラーとアクセシビリティについて知っておくべきこと | コリス
には、コントラストがWCAGを満たしているかどうかを確認するためのサイトが紹介されている。

color.review
Color.review - Colors that look and work great for everyone

実際にさっきの2つの色を設定してみると、コントラスト比と適合有無が表示される。そして下の方にある説明文が指定した色に変わり、実際に文章を見た感じをすぐに確認できる。これは地味に嬉しい点である。
早速やってみよう。上の画像の色を入力した結果は以下のとおりである。

https://color.review/check/EEE260-B4C9C9

f:id:soratokimitonoaidani:20190327231449p:plain

コントラスト比は左上に表示された値だ。コントラスト比、1.2。アウト。超アウトだ。 個人の感覚としても「何かダメな気がする」と思っていたが、数値で基準が出るので分かりやすい。

文字として明るい黄色を使いたいので、背景は暗くないといけないんだな。配色見本から別の色を使うことにした。

https://color.review/check/EEE260-555f66

f:id:soratokimitonoaidani:20190327231502p:plain

これだとコントラスト比は4.8になった。アイキャッチ画像のような大きな文字の場合、「コントラスト(高度)」の基準は4.5なので、この基準を満たしている。


「color contrast check」で検索すると、他にも同様のサイトが見つかる(が、紹介したcolor.reviewが一番見やすいと思う)。

上記のcolor.reviewと少し毛色の違うものだと、
Color Safe - accessible web color combinations
があった。最初に背景色を指定すると、基準に適合する文字色を多数提案してくれる。
「この背景色で、文字に青系の色を使いたいけど、どの色が良いんだろう」というときに便利。

それでは。

pandasでサンプルのデータセットを使う4個の方法をまとめた

pandasを使っていて、ある関数の挙動を確認するのに、ちょろっとお試しのデータセットがあれば良いなぁ、と思うことがある。

俺も以前の記事で、pandasの使い方を説明するときに、自作のデータセットを使って書いていた。
[pandas]特定の条件を満たす行を削除する - 子供の落書き帳 Renaissance
これくらい簡単なものならばまだ良いけど、いちいちデータセットを自分で作るより、すぐに読み込んで使えれば楽である。
ありがちなのはiris(アヤメ)のデータだけど、その手のサンプルのデータセットを簡単に読み込む方法はあるのだろうか?

f:id:soratokimitonoaidani:20190327225337p:plain

pandas自体にサンプルデータセットの機能は無い

pandas単体では、そのような方法は無い。……たぶん。
pandasの公式チュートリアルのページ
Getting started — pandas 1.0.3 documentation
を見ても、自分で入力した、手作りの(?)データセットを使って説明している。

ということで、他のライブラリを活用してデータセットを読み込む方法を調べた。
正直に言うと、以下の話は
python - Sample datasets in Pandas - Stack Overflow
dataset - Are there any example data sets for Python? - Stack Overflow
あたりをまとめ直して、自分で追加で調べて、コード書いて検証した、というものなので、
英語読める人は、上記のstackoverflowを見たほうが早いかもしれない。

それぞれの方法について、利用できる主なデータ(俺の主観)を挙げた。

pandas:urlを指定してread_csv

pandas.read_csv — pandas 1.0.3 documentation
read_csv関数は引数としてURLを取ることができる。
したがって、read_csv関数を使えばよい。

# pd.read_csv('https://github.com/mwaskom/seaborn-data/blob/master/iris.csv')
# はエラーになる
data = pd.read_csv('https://raw.githubusercontent.com/mwaskom/seaborn-data/master/iris.csv')
print(data.head())
   sepal_length  sepal_width  petal_length  petal_width species
0           5.1          3.5           1.4          0.2  setosa
1           4.9          3.0           1.4          0.2  setosa
2           4.7          3.2           1.3          0.2  setosa
3           4.6          3.1           1.5          0.2  setosa
4           5.0          3.6           1.4          0.2  setosa

seaborn

seaborn.load_dataset — seaborn 0.10.1 documentation というのが、データをロードする関数だ。

おおもとのデータは
GitHub - mwaskom/seaborn-data: Data repository for seaborn examples
にある。ここには「seabornのドキュメントのためのサンプルなので、ドキュメントにとって不要と判断したらデータセットを変更・削除する可能性もあるよ」と書かれている。

import seaborn as sns

# 利用できるデータセットの一覧を表示
sns.get_dataset_names()
(警告が出たけど省略)
['anscombe',
 'attention',
 'brain_networks',
 'car_crashes',
 'diamonds',
 'dots',
 'exercise',
 'flights',
 'fmri',
 'gammas',
 'iris',
 'mpg',
 'planets',
 'tips',
 'titanic']

GitHubを見ても、どのデータセットがどういうものなのか書いてない……他の文献などを見て判明したのは以下の通り。

titanic.csvもあるが、kaggleのtitanicと大筋は似ているけど細部が違うようだ。(例えば、seabornのtitanicには乗客の氏名がない)

おなじみirisデータをダウンロードしてみよう。

iris = sns.load_dataset('iris')
print(iris.head())
print(type(iris))
   sepal_length  sepal_width  petal_length  petal_width species
0           5.1          3.5           1.4          0.2  setosa
1           4.9          3.0           1.4          0.2  setosa
2           4.7          3.2           1.3          0.2  setosa
3           4.6          3.1           1.5          0.2  setosa
4           5.0          3.6           1.4          0.2  setosa
<class 'pandas.core.frame.DataFrame'>

関数の返り値が既にDataFrame型なので、変換の必要はない。

scikit-learn

scikit-learnでロードできるデータセットは、
7. Dataset loading utilities — scikit-learn 0.22.2 documentation
サンプル用のデータセット(Toy datasets)の他にも、

などがあるが、今回は省略。詳細は上記リンクを参照してほしい。

Toy datasetsにリストアップされてるのは全7種類である。本などでは、
iris(アヤメのデータ)
boston(ボストンの住宅価格のデータ)
あたりを見かける事が多い気がする。

from sklearn import datasets

iris = datasets.load_iris()
sample_data = iris['data']
print(pd.DataFrame(sample_data).head())
     0    1    2    3
0  5.1  3.5  1.4  0.2
1  4.9  3.0  1.4  0.2
2  4.7  3.2  1.3  0.2
3  4.6  3.1  1.5  0.2
4  5.0  3.6  1.4  0.2

このままでは列の名前が分からないので、DataFrame作成時にcolumnで指定する。

print(pd.DataFrame(sample_data,
            columns=iris['feature_names']).head())
   sepal length (cm)  sepal width (cm)  petal length (cm)  petal width (cm)
0                5.1               3.5                1.4               0.2
1                4.9               3.0                1.4               0.2
2                4.7               3.2                1.3               0.2
3                4.6               3.1                1.5               0.2
4                5.0               3.6                1.4               0.2

正解ラベルはtargetの中に入っている。

print(pd.Series(iris['target']).head())
print(iris['target_names'])
0    0
1    0
2    0
3    0
4    0
dtype: int32
['setosa' 'versicolor' 'virginica']

statsmodels経由でR datasets

「statsmodelsは、多くの統計モデルの最適化の実行や、統計検定の実施、データの探索や可視化を行うためのPythonのライブラリです」(Pythonによるデータ分析入門 13.3)
……らしいが、今回は統計分析はしない。
専らデータのロード用としてstatsmodelsを使う。statsmodelsを使えばRのパッケージに付属するデータをロードできる。
The Datasets Package — statsmodels

statsmodels.datasets.get_rdataset関数を使うと、
http://vincentarelbundock.github.io/Rdatasets/ の中にあるデータをロードできる。

おなじみのirisは、 パッケージ名'datasets'、データセット名'iris'なので、以下のようになる。

import statsmodels.api as sm
iris = sm.datasets.get_rdataset("iris", "datasets")
print(type(iris))
<class 'statsmodels.datasets.utils.Dataset'>

実際のデータは、このdata属性の中に格納されている、と以下に書いてある。 statsmodels.datasets.get_rdataset — statsmodels

print(iris.data.head())
   Sepal.Length  Sepal.Width  Petal.Length  Petal.Width Species
0           5.1          3.5           1.4          0.2  setosa
1           4.9          3.0           1.4          0.2  setosa
2           4.7          3.2           1.3          0.2  setosa
3           4.6          3.1           1.5          0.2  setosa
4           5.0          3.6           1.4          0.2  setosa

というわけでirisをロードできた。……だが、1200個以上のデータセットを使えるところがRdatasets利点だと思うので、iris程度ならわざわざこれを使わなくても良いと思った。

ここまで見てきたけど、同じirisのデータでも、特徴量の名称、正解ラベルが一緒か別か、など、微妙に形式は異なっている。

まとめ

  • pandasでサンプルのデータセットを読み込む方法を調べた
  • pandasのみで読み込む方法は無いので、他のライブラリから呼び出す必要がある
  • ライブラリごとに読み込んだデータの形式は異なるので、ドキュメントなどを参照して処理するとよさそう

以上。それでは。

pandasのSettingWithCopyWarningを理解する (2/3)

この記事は、 SettingwithCopyWarning: How to Fix This Warning in Pandas – Dataquest の日本語訳です。3回にわたって掲載予定で、この記事は2回目です。

1回目の記事はこちら: linus-mk.hatenablog.com

f:id:soratokimitonoaidani:20190314234733p:plain

SettingWithCopyWarningを処理するためのヒントとコツ

以下でもっと詳細な分析をする前に、顕微鏡を取り出して、SettingWithCopyWarningの細かい点と核心的な点をいくつか見てみよう。

警告を消す

最初に、この記事を完全なものにするためには、SettingWithCopyの設定を明示的に制御する方法を説明せねばなるまい。pandas内のmode.chained_assignmentオプションは、以下のいずれかの値を取ることができる。

  • 'raise' - 警告の代わりに例外を上げる。
  • 'warn' - 警告を生成する(デフォルト)。
  • None - 警告を完全にオフにする。

たとえば、警告をオフにしてみよう。

pd.set_option('mode.chained_assignment', None)
data[data.bidder == 'parakeet2004']['bidderrate'] = 100

この設定では警告が一切発生しないので、あなたが自分がしていることを十分に理解しているのでない限り、この設定は推奨されない。あなたがほんのわずかでも疑問を感じるのであれば、これはお勧めできない。SettingWithCopyを非常に重要なものと考えて、警告ではなく例外に昇格させることを選ぶ開発者もいる。以下のようになる:

pd.set_option('mode.chained_assignment', 'raise')
data[data.bidder == 'parakeet2004']['bidderrate'] = 100
---------------------------------------------------------------------------
SettingWithCopyError                      Traceback (most recent call last)
<ipython-input-13-80e3669cab86> in <module>()
      1 pd.set_option('mode.chained_assignment', 'raise')
----> 2 data[data.bidder == 'parakeet2004']['bidderrate'] = 100

/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/pandas/core/frame.py in __setitem__(self, key, value)
   2427         else:
   2428             # set column
-> 2429             self._set_item(key, value)
   2430 
   2431     def _setitem_slice(self, key, value):

/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/pandas/core/frame.py in _set_item(self, key, value)
   2500         # value exception to occur first
   2501         if len(self):
-> 2502             self._check_setitem_copy()
   2503 
   2504     def insert(self, loc, column, value, allow_duplicates=False):

/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/pandas/core/generic.py in _check_setitem_copy(self, stacklevel, t, force)
   1758 
   1759             if value == 'raise':
-> 1760                 raise SettingWithCopyError(t)
   1761             elif value == 'warn':
   1762                 warnings.warn(t, SettingWithCopyWarning, stacklevel=stacklevel)

SettingWithCopyError: 
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

See the caveats in the documentation: http://pandas.pydata.org/pandas-docs/stable/indexing.html#indexing-view-versus-copy

あなたのチームでpandasの経験の浅い開発者と一緒のプロジェクトで作業をしている場合、またはその完全性において高いレベルの厳格さまたは確実性を必要とするプロジェクトで作業をしている場合、これは特に役に立つ。

この設定を使用するより正確な方法は、コンテキストマネージャを使用することである。

# resets the option we set in the previous code segment
pd.reset_option('mode.chained_assignment')

with pd.option_context('mode.chained_assignment', None):
    data[data.bidder == 'parakeet2004']['bidderrate'] = 100

コードを見れば分かるように、このアプローチをとれば、一律に環境全体に影響を及ぼすのではなく、警告を細かく抑制することができる。

is_copyプロパティ

この警告を回避するためのやり方がもう一つある。それはSettingWithCopyシナリオを解釈するためにpandasが使っているツールの1つを変更することである。 それぞれのDataFrameは、is_copyプロパティを持ち、デフォルトではNoneである。コピーである場合は、is_copyプロパティはソースのDataFrameを参照するためにweakrefを使用する。is_copyNoneに設定すると、警告を生成しないようにすることができる。

winners = data.loc[data.bid == data.price]
winners.is_copy = None
winners.loc[304, 'bidder'] = 'therealname'

しかしながら、これは奇跡的に問題を解決するわけではなく、バグの検出が非常に困難になる可能性がある。

single-dtyped 対 multi-dtyped オブジェクト

強調しておく価値のあるさらなる点は、single-dtypedオブジェクトとmulti-dtypedオブジェクトの区別である。DataFrameは、そのすべての列が同じdtypeの場合、single-dtypedである。例えば:

import numpy as np

single_dtype_df = pd.DataFrame(np.random.rand(5,2), columns=list('AB'))
print(single_dtype_df.dtypes)
single_dtype_df
A    float64
B    float64
dtype: object
- A B
0 0.383197 0.895652
1 0.077943 0.905245
2 0.452151 0.677482
3 0.533288 0.768252
4 0.389799 0.674594

一方、 DataFrameは、そのすべての列のうち同じdtypeではないものがある場合、multi-dtypedである。たとえば:

multiple_dtype_df = pd.DataFrame({'A': np.random.rand(5),'B': list('abcde')})
print(multiple_dtype_df.dtypes)
multiple_dtype_df
A    float64
B     object
dtype: object
- A B
0 0.615487 a
1 0.946149 b
2 0.701231 c
3 0.756522 d
4 0.481719 e

以下の「歴史」のセクション(※訳注:第3回で掲載予定)で説明している理由から、multi-dtypedオブジェクトに対するindexer-get操作は常にコピーを返す。 ただし、主に効率のために、single-dtyped型オブジェクトに対するindexer-get操作はほとんどの場合ビューを返す。ここで注意すべき点は、これ(ビューとコピーのどちらが返るのか)はオブジェクトのメモリレイアウトに依存し、保証されていないということである。

偽陽性

(訳注:ここでの「偽陽性」は「実際には問題が起きていないのに、警告が発生すること」を指す)

偽陽性、すなわち連鎖割り当てが実際には起きていないのに報告されている状況は、以前のバージョンのpandasではよりよくあることだったが、その後はほとんど解決されている。完全を期すために、現在は修正された偽陽性の例をここに述べておいた方がよいだろう。pandasの以前のバージョンで以下のような状況が発生した場合は、警告を無視しても抑制しても安全である(またはアップグレードすることで完全に回避できる)。

現在の列の値を使用してDataFrameに新しい列を追加すると、警告が発生していたが、これは修正された。

data['bidtime_hours'] = data.bidtime.map(lambda x: x * 24)
data.head(2)
- auctionid bid bidtime bidder bidderrate openbid price bidtime_hours
0 8213034705 95.0 2.927373 jake7870 0 95.0 117.5 70.256952
1 8213034705 115.0 2.943484 davidbresler2 1 95.0 117.5 70.643616

最近まで、DataFrameのスライスに対してapplyメソッドを使って値を設定するときにも、偽陽性が発生したが、これも修正されている。

data.loc[:, 'bidtime_hours'] = data.bidtime.apply(lambda x: x * 24)
data.head(2)
- auctionid bid bidtime bidder bidderrate openbid price bidtime_hours
0 8213034705 95.0 2.927373 jake7870 0 95.0 117.5 70.256952
1 8213034705 115.0 2.943484 davidbresler2 1 95.0 117.5 70.643616

そして最後に、バージョン0.17.0までは、DataFrame.sampleメソッド に誤ったSettingWithCopyの警告を引き起こすバグが存在した。現在では、sampleメソッドは常にコピーを返す。

sample = data.sample(2)
sample.loc[:, 'price'] = 120
sample.head()
- auctionid bid bidtime bidder bidderrate openbid price bidtime_hours
481 8215408023 91.01 2.990741 sailer4eva 1 0.99 120 71.777784
503 8215571039 100.00 1.965463 lambonius1 0 50.00 120 47.171112

2019年5月18日 追記:

3回目 linus-mk.hatenablog.com

Python公式ドキュメントをアンダーバーのある語句で検索しても一部しか表示されない

軽めの記事。

1つ前の記事を書くときに、Python公式ドキュメントを検索した。 __doc__とか__dict__とか__name__とかで検索して、解説を探した。 そうしたら、表示されたページの数があまりにも少ない。

英語のドキュメントを日本語に翻訳するときも、この手の単語は変わらずそのままである。したがって英語で検索したときも日本語で検索したときも、ヒット件数はほぼ同じになるはずだ。しかし実際には、日本語のほうが件数がかなり少ない。

あとはスクリーンショットを貼っておくので、ご覧ください。

__doc__の検索結果(日本語) f:id:soratokimitonoaidani:20190309154944p:plain

__doc__の検索結果(英語) f:id:soratokimitonoaidani:20190309155010p:plain

__name__の検索結果(日本語) f:id:soratokimitonoaidani:20190309155001p:plain

__name__の検索結果(英語) f:id:soratokimitonoaidani:20190309155005p:plain

以上。それでは。

pythonのクイズ、My Python Quizをやって復習した(その2)

前回に引き続いて、My Python Quizの21~40問目を解いて、分からなかった問題を復習した。

www.mypythonquiz.com

  • 選択肢の中から1つを選ぶ問題。
  • ほとんどの問題は「What gets printed?(以下のコードの結果、何が表示されるか)」という問題である。別の形式の場合はその都度説明する。 ただし選択肢の中に「実行時にエラーが発生する」が含まれる場合もある。

Question #27:
class Person:
    def __init__(self, id):
        self.id = id

obama = Person(100)

obama.__dict__['age'] = 49

print(obama.age + len(obama.__dict__))

object.__dict__
オブジェクトの (書き込み可能な) 属性を保存するために使われる辞書またはその他のマッピングオブジェクトです。
組み込み型 — Python 3.7.2 ドキュメント

見慣れない__dict__って何なんだ、と思ったら、オブジェクトの属性を辞書にして保持してたのね。下記のように属性が入るので、len(obama.__dict__)が2になるので答えは51だ。

>>> obama.__dict__
{'id': 100, 'age': 49}

Question #29: 
def simpleFunction():
    "This is a cool simple function that returns 1"
    return 1

print(simpleFunction.__doc__[10:14])

関数の説明を書いて、それを__doc__属性で呼び出せるよ、という話。
4. その他の制御フローツール — Python 3.7.2 ドキュメント あたりを参照。

公式のドキュメントにわかりやすい記述が見つからなかったので、以下のページをを参考に挙げる。
Pythonのdocstring(ドキュメンテーション文字列)の書き方 | note.nkmk.me


Question #30: What does the code below do?
sys.path.append('/root/mods')

sys.path
モジュールを検索するパスを示す文字列のリスト。 PYTHONPATH 環境変数と、インストール時に指定したデフォルトパスで初期化されます。
sys --- システムパラメータと関数 — Python 3.7.2 ドキュメント

sys.pathはリストなので、これに対してappendメソッドを呼ぶと要素を追加できる。新たにフォルダパスを追加すると、モジュールを検索するときにそのフォルダも探すようになる。


Question #33: True or false? Code indentation must be 4 spaces when creating a code block?
if error:
    # four spaces of indent are used to create the block
    print(msg)

そう言われたら空白4つじゃなきゃいけない気がしてきて、Trueを選んでしまった……(正解はFalse) Pythonのコーディング規約であるPEP8では、スペース4つを使うように書いてある。
しかし、言語の仕様としては別にスペースは4つでなくてもよい。

1レベルインデントするごとに、スペースを4つ使いましょう。
はじめに — pep8-ja 1.0 ドキュメント


Question #34: Assuming the filename for the code below is /usr/lib/python/person.py
and the program is run as: 
python /usr/lib/python/person.py 

What gets printed?
class Person:
    def __init__(self):
        pass

    def getAge(self):
        print(__name__)

p = Person()
p.getAge()

print(__name__)によって何が起きるか、という話。公式ドキュメントには上手くまとまった解説が見当たらなかったので、PyQから引用する。

  • import hello した:hello.py 内部で __name__ は "hello" という文字列になる
  • python hello.py した:hello.py 内部で __name__ は "__main__" という文字列になる

Pythonのif __name__ == "__main__" とは何ですか?への回答 - PyQオフィシャルブログ

設問の条件より、今回は後者に該当するので、print(__name__)とすると__main__という文字列が出力される。


Question #39: 
confusion = {}
confusion[1] = 1
confusion['1'] = 2
confusion[1.0] = 4

sum = 0
for k in confusion:
    sum += confusion[k]

print(sum)

このpython quizにたどり着いたきっかけとなった設問である。

もしふたつの数値が (例えば 1 と 1.0 のように) 等しければ、同じ辞書の項目として互換的に使用できます。 (ただし、コンピュータは浮動小数点数を近似値として保管するので、辞書型のキーとして使用するのはたいてい賢くありません。)
https://docs.python.org/ja/3/library/stdtypes.html#mapping-types-dict

さらにいうと、辞書のキーが一致するかを調べているのはhash()関数である。

hash(object) オブジェクトのハッシュ値を (存在すれば) 返します。ハッシュ値は整数です。これらは辞書を検索する際に辞書のキーを高速に比較するために使われます。等しい値となる数値は等しいハッシュ値を持ちます (1 と 1.0 のように型が異なっていてもです)。

これにより、1と1.0のハッシュ値は等しいので、キーとして一致する。

>>> 1 == 1.0
True
>>> hash(1) == hash(1.0)
True
>>> confusion
{1: 4, '1': 2}

Question #40: What gets printed?
boxes = {}
jars = {}
crates = {}

boxes['cereal'] = 1
boxes['candy'] = 2
jars['honey'] = 4
crates['boxes'] = boxes
crates['jars'] = jars

print(len(crates[boxes]))

何かややこしくてよく分からない問題だったけど、crates[boxes]crates['boxes']が別物だって分かってるかって話で良いのかな?
実際に打ち込んでみると以下のようにエラーが起きる。

>>> print(len(crates[boxes]))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'dict'

辞書のキーは ほぼ 任意の値です。ハッシュ可能 (hashable) でない値、つまり、リストや辞書その他のミュータブルな型 (オブジェクトの同一性ではなく値で比較されるもの) はキーとして使用できません
組み込み型 — Python 3.7.2 ドキュメント

  • 辞書のキーには、ハッシュ可能な値を使用することが可能、ハッシュ不可能な値を使用することはできないよ
  • 今回crates[boxes]って書いたけど、boxesは辞書型だからハッシュ不可能だよ
  • だからエラーが返るよ

という理屈だと思う。

pythonのクイズ、My Python Quizをやって復習した

ネットをいろいろ見ていて、My Python Quizというサイトにたどり着いた *1

www.mypythonquiz.com

このサイトをなんて呼ぶのが良いのだろうか?
「テスト/試験」って書くと何かユニットテストの話に見えてしまうし。
「練習問題」って書くと「では素数判定を実装してください」みたいなものを想像しがちである。
「クイズ」と言うのが正しい気がする。
具体的な問題をQ&A形式で質問されるので、選択肢を選んで答えればよい。問題の具体的な内容は下にある。

  • 選択肢の中から1つを選ぶ問題。
  • ほとんどの問題は「What gets printed?(以下のコードの結果、何が表示されるか)」という問題である。別の形式の場合はその都度説明する。 ただし選択肢の中に「実行時にエラーが発生する」が含まれる場合もある。

20問まで解いてみて分からなかった問題を復習した。


Question #3:

def f(): pass
print(type(f()))

<class 'function'>じゃないのかと思ったけど違った。盛大に勘違いをしていた。

>>> def f():pass
...
>>> print(type(f()))
<class 'NoneType'>
>>> print(type(f))
<class 'function'>

fではなくてf()なので関数の戻り値の型を表示している。returnがないのでf()はNoneで、その型なのでNonetypeとなる。


Question #4: 
print(type(1J))

数値リテラルに 'j' または 'J' をつけると虚数 (実部がゼロの複素数) を与え、それに整数や浮動小数点数を加えて実部と虚部を持つ複素数を得られます。
4. 組み込み型 — Python 3.6.5 ドキュメント

というわけで複素数型で、クラスは<class 'complex'>です。


Question #5:
print(type(lambda:None))

Question #9:
x = 4.5
y = 2
print(x//y)

整数の除算を行うから出力は「2」かと思ったけど、正解は「2.0」。

Python は型混合の算術演算に完全に対応しています: ある二項算術演算子の被演算子の数値型が互いに異なるとき、より "制限された" 型の被演算子は他方の型に合わせて広げられます。ここで整数は浮動小数点数より制限されており、浮動小数点数複素数より制限されています。
整数の除算とも呼ばれます。結果の型は整数型とは限りませんが、結果の値は整数です。結果は常に負の無限大の方向に丸められます
4. 組み込み型 — Python 3.6.5 ドキュメント

浮動小数点として演算した後で、(数学の)floor関数(=床関数、ガウス記号の関数)を適用すれば良いようだ。
で、int < float < complexという型の「大小関係」があり、演算結果はより右側の型になる。それは整数除算であっても変わらない。だから今回の結果ではfloat型の2.0が出力される、ということね。


Question #11:
x = True
y = False
z = False

if x or y and z:
    print("yes")
else:
    print("no")
Question #12:
x = True
y = False
z = False

if not x or y:
    print(1)
elif not x or not y and z:
    print(2)
elif not x or y or not y and x:
    print(3)
else:
    print(4)

not演算子、and演算子、or演算子はこの順に優先順位が高く、先に評価される。


Question #13: If PYTHONPATH is set in the environment, which directories are searched for modules?
A) PYTHONPATH directory
B) current directory
C) home directory
D) installation dependent default path

ABCDの中から複数回答。

sys.path は以下の場所に初期化されます:

6. モジュール (module) — Python 3.6.5 ドキュメント

したがって、それぞれB,A,Dとなる。


Question #15:
print(r"\nwoow")

文字列リテラルとバイト列リテラルの両方は、任意で文字 'r' または 'R' をプレフィックスに持つことができます; そのような文字列は raw strings と呼ばれ、バックスラッシュをリテラル文字として扱います。
2. 字句解析 — Python 3.6.5 ドキュメント

rがプレフィックスに存在するので、raw stringとなり、バックスラッシュはそのまま表示される。その結果、\nwoowと表示される。


Question #16:
print("\x48\x49!")

\xhh 16進数値 hh を持つ文字
2. 字句解析 — Python 3.6.5 ドキュメント

この動作はC言語とよく似ている。


Question #17:
print(0xA + 0xa)

16進数の数値リテラルだろうとは想像がついたけど、16進数のまま出力されると勘違いして14(16進数)と答えてしまった。10進数で出力するので20(10進数)が正解である。


Question #18: What gets printed?
class parent:
    def __init__(self, param):
        self.v1 = param

class child(parent):
    def __init__(self, param):
        self.v2 = param

obj = child(11)
print(obj.v1 + " " + obj.v2)
>>> print(obj.v1 + " " + obj.v2)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'child' object has no attribute 'v1'

うっかりしてた。 parentクラスの__init__()を呼んでいないので、子クラスでは変数v1が定義されない。 C++だったらメンバ変数の定義はするから、親クラスのコンストラクタを通らなくても「メンバ変数が存在しない」という状況は発生しない。しかしPythonだと、こういう事態も起きるんだな。