# python_course2
## 本章の目的
1. 基本的なデータ構造(リスト、タプル、辞書、集合)を使えるようになること
1. オブジェクトとクラスが何かを理解し使えるようになること
## データ構造
データの集計や加工を行う場合、集合に対する反復的な処理が必要となります。例えば、月間の売上を算出したい場合、日々の売上を1つ1つ足し合わせる必要があるでしょう。このとき、日々の売上を別々の変数に格納するよりも、すべての日々の売上を1つの変数に格納できたほうが記述量が少なくなり、また意味が明確になるため、より便利だと考えられます。そのような用途のため、Pythonでは様々なデータ構造が用意されています。
### リスト
集合を順序付けて扱いたい場合があります。例として、ある週における日次売上の累計を時系列に計算することを考えます。このとき、週の始めから対象日までの合計売上を、日付順に並べる必要があるでしょう。このような場合には、リストを使うと便利です。以下に記述例を示します。
###### 実行コード
```
daily_sales_list = [1, 1, 1, 1, 1, 1, 1]
day_from_start = 0
daily_total_sales = 0
while day_from_start < len(daily_sales_list):
daily_sales = daily_sales_list[day_from_start]
daily_total_sales += daily_sales
print(f'{day_from_start + 1}日目までの売上は{daily_total_sales}万円です。')
day_from_start += 1
```
###### 実行結果
```
1日目までの売上は1万円です。
2日目までの売上は2万円です。
3日目までの売上は3万円です。
4日目までの売上は4万円です。
5日目までの売上は5万円です。
6日目までの売上は6万円です。
7日目までの売上は7万円です。
```
この例では、リストの値を順に取得し、それまでのリストの値の合計に追加することで、時系列の累計売上を計算しています。具体的なリストの記述方法になるため、特に実行コードの1, 4, 5行目に注目してください。まず、1行目で週の売上をリストとして変数に格納しています。次に、4行目においてlen関数によりリストの長さを取得し、開始からの経過日がリストの長さより小さいことをwhile文の終了条件としています。さらに、5行目においてリストの要素を取得しています。
#### リストを作成する
リストは、0個以上の要素(文字列や数値など任意のデータ)をカンマで区切り、角かっこにより全体を囲むことで作成できます。また、空リスト(要素のないリスト)の作成にはlist関数も利用できます。以下に例を示します。
###### 実行コード
```
day_of_weeks = []
print(day_of_weeks)
day_of_weeks = list()
print(day_of_weeks)
day_of_weeks = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']
print(day_of_weeks)
```
###### 実行結果
```
[]
[]
['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']
```
角かっこもしくはlist関数により、空リストや曜日文字列のリストを作成できることが確認できます。
#### リストの要素を取得する
リストの要素はオフセット(順番)を指定することで取得できます。オフセットには正負両方の整数を指定することができ、正の場合には正順(左から順)、負の場合には逆順(右から順)で数えた番号の要素を取得します。このとき、要素数を超えるようなオフセットを指定した場合にはエラーとなるため注意してください。以下に例を示します。
###### 実行コード
```
day_of_weeks = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']
print('正順にリストの要素を表示します。')
count = 0
while count < len(day_of_weeks):
print(day_of_weeks[count])
count += 1
print()
count = 0
print('逆順にリストの要素を表示します。')
while count < len(day_of_weeks):
print(day_of_weeks[-count - 1])
count += 1
print()
print('不正なリストのオフセットです。')
print(day_of_weeks[7])
```
###### 実行結果
```
正順にリストの要素を表示します。
Monday
Tuesday
Wednesday
Thursday
Friday
Saturday
Sunday
逆順にリストの要素を表示します。
Sunday
Saturday
Friday
Thursday
Wednesday
Tuesday
Monday
不正なリストのオフセットです。
Traceback (most recent call last):
File "sample.py", line 20, in <module>
print(day_of_weeks[7])
IndexError: list index out of range
```
リストの要素をオフセットを指定することで取得できること、正負によって正順または逆順を切り替えられること、そして不正なオフセットを指定するとエラーになることが確認できます。
##### 複数要素の取得
日々の売上から平日の売上合計を計算する場合など、リストの複数要素を取得したいことがあります。このような場合は、スライス(記法)を使うと便利です。スライスでは開始番号と終了番号、そしてステップ(何個飛ばしで取得するかを示す量)を指定し、任意の範囲の要素を任意の間隔で取得することができます。なお、スライスの構成要素(開始番号、終了番号、ステップ)は省略可能で、内部的に決められた値が適用されます。以下にスライスの書式と実行コード、実行結果を示します。
###### スライスの書式
```
リスト[開始番号:終了番号:ステップ]
```
###### 実行コード
```
daily_sales_in_half_month = [1, 2, 3, 4, 5, 6, 7, 1, 2, 3, 4, 5, 6, 7]
weekday_sales_of_first_week = daily_sales_in_half_month[0:5]
weekday_sales_of_second_week = daily_sales_in_half_month[7:12]
weekday_total_sales_of_first_week = weekday_sales_of_first_week[0] + \
weekday_sales_of_first_week[1] + \
weekday_sales_of_first_week[2] + \
weekday_sales_of_first_week[3] + \
weekday_sales_of_first_week[4]
weekday_total_sales_of_second_week = weekday_sales_of_second_week[0] + \
weekday_sales_of_second_week[1] + \
weekday_sales_of_second_week[2] + \
weekday_sales_of_second_week[3] + \
weekday_sales_of_second_week[4]
weekday_total_sales = weekday_total_sales_of_first_week + weekday_total_sales_of_second_week
print(f'平日の合計売上: {weekday_total_sales}')
monday_sales = daily_sales_in_half_month[::7]
monday_total_sales = monday_sales[0] + monday_sales[1]
print(f'各月曜日の売上: {monday_sales}')
print(f'月曜日の合計売上: {monday_total_sales}')
```
###### 実行結果
```
平日の合計売上: 30
各月曜日の売上: [1, 1]
月曜日の合計売上: 2
```
スライスを使用している3, 18行目に注目してください(4行目は3行目と同様のため割愛します)。3行目では開始番号と終了番号を指定してリストの要素を範囲取得しており、0から4番目までの要素が取得されます。最後の要素は「終了番号-1」番目となり、4番目までの要素を取得するために5を指定していることに注意してください。18行目では、開始番号と終了番号が省略され、ステップのみが指定されています。この記述により、全リスト要素が最初の要素含め7個ずつ取得されます。
#### リストに要素を追加する
日次売上の履歴に新しい売上データを追加する場合など、リストに要素を追加したいことがあります。このような要素を一個だけ追加する処理は、appendメソッドにより記述できます。以下に例を示します。
###### 実行コード
```
daily_sales_in_half_month = [1, 2, 3, 4, 5, 6, 7, 1, 2, 3, 4, 5, 6, 7]
print(daily_sales_in_half_month)
daily_sales_in_half_month.append(1)
print(daily_sales_in_half_month)
```
###### 実行結果
```
[1, 2, 3, 4, 5, 6, 7, 1, 2, 3, 4, 5, 6, 7]
[1, 2, 3, 4, 5, 6, 7, 1, 2, 3, 4, 5, 6, 7, 1]
```
appendメソッドにより、daily_sales_in_half_monthに新たな値が追加されたことを確認できます。
##### リスト(複数要素)の追加
要素追加を繰り返し適用することが冗長な場合には、複数要素をリストにまとめ、一括で追加することもできます。そのためには、+演算子やextendメソッドを利用できます。以下に例を示します。
###### 実行コード
```
daily_sales_in_first_half_of_month = [1, 2, 3, 4, 5, 6, 7, 1, 2, 3, 4, 5, 6, 7]
daily_sales_in_second_half_of_month = [7, 6, 5, 4, 3, 2, 1, 7, 6, 5, 4, 3, 2, 1]
daily_sales_in_month = daily_sales_in_first_half_of_month + daily_sales_in_second_half_of_month
print(daily_sales_in_first_half_of_month)
print(daily_sales_in_second_half_of_month)
print(daily_sales_in_month)
print()
daily_sales_in_month = []
daily_sales_in_month.extend(daily_sales_in_first_half_of_month)
daily_sales_in_month.extend(daily_sales_in_second_half_of_month)
print(daily_sales_in_first_half_of_month)
print(daily_sales_in_second_half_of_month)
print(daily_sales_in_month)
```
###### 実行結果
```
[1, 2, 3, 4, 5, 6, 7, 1, 2, 3, 4, 5, 6, 7]
[7, 6, 5, 4, 3, 2, 1, 7, 6, 5, 4, 3, 2, 1]
[1, 2, 3, 4, 5, 6, 7, 1, 2, 3, 4, 5, 6, 7, 7, 6, 5, 4, 3, 2, 1, 7, 6, 5, 4, 3, 2, 1]
[1, 2, 3, 4, 5, 6, 7, 1, 2, 3, 4, 5, 6, 7]
[7, 6, 5, 4, 3, 2, 1, 7, 6, 5, 4, 3, 2, 1]
[1, 2, 3, 4, 5, 6, 7, 1, 2, 3, 4, 5, 6, 7, 7, 6, 5, 4, 3, 2, 1, 7, 6, 5, 4, 3, 2, 1]
```
+演算子もしくはextendメソッドにより、daily_sales_in_monthにデータが一括追加されることを確認できます。このように、+演算子とextendメソッドのどちらを利用しても効果は同じです。ただし、extendは変数の中身が変化するためプログラムの意図しない挙動を発生させることがあるため(このように中身が変更可能なことをミュータブルと呼びます)、注意してください。
##### リスト要素の挿入
要素に順序がある場合など、(末尾への追加ではなく)要素を任意の位置に挿入したいことがあります。このような場合には、insertメソッドを利用できます。以下に例を示します。
###### 実行コード
```
child_morning_tasks = ['breakfast', 'go to school']
print(child_morning_tasks)
child_morning_tasks.insert(0, 'wash face')
print(child_morning_tasks)
```
###### 実行結果
```
['breakfast', 'go to school']
['wash face', 'breakfast', 'go to school']
```
Insertメソッドにより、child_morning_tasksの先頭にデータが追加されることを確認できます。
#### リストの要素を更新する
間違った値を修正したい場合などには、オフセットでリスト要素の値を取得できるのと同様に、オフセットでリスト要素の値を更新できます。以下に例を示します。
###### 実行コード
```
day_of_weeks = ['Monday', 'Wrong Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']
print(day_of_weeks)
day_of_weeks[1] = 'Tuesday'
print('Wrong Tuesdayを修正しました。')
print(day_of_weeks)
```
###### 実行結果
```
['Monday', 'Wrong Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']
Wrong Tuesdayを修正しました。
['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']
```
オフセットを指定した代入操作によって、day_of_weeksのデータが更新されたことを確認できます。
#### リストから要素を削除する
退会ユーザのデータを削除する場合など、リストの要素を削除するためには、del文(リストのメソッドではないので注意してください)を利用できます。del文ではオフセットの指定により任意の番号を指定できますが、末尾以外の番号が指定された場合には、後ろの要素は1つずつ前に移動するため注意してください。以下に例を示します。
###### 実行コード
```
users = ['taro', 'hanako', 'jiro', 'tetsuko', 'saburo', 'ritsuko']
print(f'会員リスト: {users}')
del users[1]
print('hanakoが退会しました。')
print(f'会員リスト: {users}')
```
###### 実行結果
```
会員リスト: ['taro', 'hanako', 'jiro', 'tetsuko', 'saburo', 'ritsuko']
hanakoが退会しました。
会員リスト: ['taro', 'jiro', 'tetsuko', 'saburo', 'ritsuko']
```
delメソッドにより、usersの2番目のデータが削除されたことを確認できます。3番目以降のデータが1つずつ前に移動していることに注意してください。
##### 値による要素の削除
効果は同じですが、オフセットではなく値で削除する要素を指定することもできます。この場合には、removeメソッド(こちらはdel文と違いリストのメソッドです)を利用できます。以下に例を示します。
###### 実行コード
```
users = ['taro', 'hanako', 'jiro', 'tetsuko', 'saburo', 'ritsuko']
print(f'会員リスト: {users}')
users.remove('hanako')
print('hanakoが退会しました。')
print(f'会員リスト: {users}')
```
###### 実行結果
```
会員リスト: ['taro', 'hanako', 'jiro', 'tetsuko', 'saburo', 'ritsuko']
hanakoが退会しました。
会員リスト: ['taro', 'jiro', 'tetsuko', 'saburo', 'ritsuko']
```
removeメソッドにより、hanakoが削除されたことを確認できます。
#### リストを走査する
集計処理、加工処理を行うなどのため、リストに対する反復的な処理(走査)が必要となることがあります。そのような場合には、リストをループ文と組み合わせて使用します。以下に例を示します。
###### 実行コード
```
users = ['taro', 'hanako', 'jiro', 'tetsuko', 'saburo', 'ritsuko']
for user in users:
print(f'{user}')
```
###### 実行結果
```
taro
hanako
jiro
tetsuko
saburo
ritsuko
```
for〜in文をユーザリストに対して使用し、全ユーザが表示されることを確認できます。
##### リストの長さを取得する
範囲外のオフセットへのアクセス防止や、データ件数の確認のため、リストの要素数を取得したいことがあります。そのような場合には、len関数を使用します。以下に例を示します。
###### 実行コード
```
users = ['taro', 'hanako', 'jiro', 'tetsuko', 'saburo', 'ritsuko']
user_count = len(users)
print(user_count)
```
###### 実行結果
```
6
```
len関数によりusersの要素数を取得できることが確認できます。
##### 走査するリストのオフセットを作成する
処理時間の問題で要素群をまとめて処理したい場合などは、リストのオフセットを作成できると便利です。そのような場合には、range関数を使用します。range関数をlist関数の引数とすることで一定の範囲の数値列を作成でき、オフセットのリストとして使うことができます。以下に例を示します。
###### 実行コード
```
users = ['taro', 'hanako', 'jiro', 'tetsuko', 'saburo', 'ritsuko']
grouped_users = []
offsets = list(range(len(users)))
print(offsets)
for offset in offsets:
user = users[offset]
grouped_users.append(user)
if (offset + 1) % 3 == 0:
print(f'{grouped_users}')
grouped_users = []
```
###### 実行結果
```
[0, 1, 2, 3, 4, 5]
['taro', 'hanako', 'jiro']
['tetsuko', 'saburo', 'ritsuko']
```
len関数によりusersの要素数を取得し、range関数によりオフセットのリストを作成することで、一定の件数ごとにユーザがまとまって表示できることを確認できます。
#### リストの要素を検索する
集計や値のチェックのために、リストを検索するための関数が用意されています。
##### 値の有無を確認する
リストの更新や削除時、予期しないプログラムのエラーを防ぐため、値が存在するかをチェックする必要があります。そのような場合には、in文を利用できます。以下に例を示します。
###### 実行コード
```
users = ['taro', 'hanako', 'jiro', 'tetsuko', 'saburo', 'ritsuko']
is_shiro = 'shiro' in users
print(f'{is_shiro}')
```
###### 実行結果
```
False
```
in文により、usersにshiroが存在しないことを確認できます。
##### 要素の値から要素のオフセットを取得する
要素の値を更新する場合など、値からオフセットを取得できると便利です。そのような場合には、indexメソッドを利用できます。以下に例を示します。
###### 実行コード
```
users = ['taro', 'hanako', 'jiro', 'tetsuko', 'saburo', 'ritsuko']
taro_index = users.index('taro')
print(f'{taro_index}')
```
###### 実行結果
```
0
```
Indexメソッドにより、taroのオフセットが0であることを確認できます。indexメソッドは値が存在しない場合エラーとなるため注意してください。
#### リストの要素を並び替える
名前を音の順に並び替えるなど、要素をオフセットではなく値の順に並び替えたいことがあります。そのような場合には、sortやreverseメソッドを利用できます。それぞれ、sortで正順、reverseで逆順に並び替えを行うことができます。以下に例を示します。
###### 実行コード
```
users = ['taro', 'hanako', 'jiro', 'tetsuko', 'saburo', 'ritsuko']
print(users)
print('ユーザを名前の順に並び替えます。')
users.sort()
print(users)
print('ユーザを名前の逆順に並び替えます。')
users.reverse()
print(users)
```
###### 実行結果
```
['taro', 'hanako', 'jiro', 'tetsuko', 'saburo', 'ritsuko']
ユーザを名前の順に並び替えます。
['hanako', 'jiro', 'ritsuko', 'saburo', 'taro', 'tetsuko']
ユーザを名前の逆順に並び替えます。
['tetsuko', 'taro', 'saburo', 'ritsuko', 'jiro', 'hanako']
```
sortメソッドによりusersがABC順に並び替わり、reverseメソッドによりABCの逆順に並び替わったことを確認できます。
### タプル
曜日や月日など概念的に変更のないリストは、意図しないエラーを防ぐために、イミュータブル(変更不可能)にするのが望ましいです。そのような場合には、タプルを利用することができます。タプルはイミュータブルなため、定義後に取得以外の操作(追加や変更、削除)を行うことはできません。以下に例を示します。
###### 実行コード
```
day_of_weeks = ('Monday', 'Wrong Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday')
print(day_of_weeks[0])
print(day_of_weeks[-1])
print(day_of_weeks[0:5:2])
day_of_weeks[0] = 'Unsupported assignment'
```
###### 実行結果
```
Monday
Sunday
('Monday', 'Wednesday', 'Friday')
Traceback (most recent call last):
File "sample.py", line 5, in <module>
day_of_weeks[0] = 'Unsupported assignment'
TypeError: 'tuple' object does not support item assignment
```
2〜4行目のとおり、リストと同様にオフセットやスライスを使用して値の取得を行えます。一方、5行目で値の代入を行おうとしていますが、変更は許可されない操作のためエラーとなります。
#### タプルとリストの違い
タプルはリストと比較して機能が少ないため、順序付きの集合を扱う場合にはリストを使用することが大半だと思われますが、参考までにタプルの利点を以下に列挙します。
- イミュータブルなため、意図しないエラーが起こりにくい
- 機能が少ないが故に少スペースで高速に動作する
- 辞書のキーとして使用できる(後述)
### 辞書
名前で集合の要素を検索できるようにしたい場合には、辞書を使うと便利です。辞書では要素の順序が管理されず、オフセットの代わりに一意のキーによって値を取得できます。キーのデータ型はイミュータブルであればよく、文字列や数値、タプルなど、様々なデータ型を扱えます。
#### 辞書を作成する
辞書はキーと値のペアをカンマで区切り、波かっこもしくはdict関数により全体を囲むことで簡単に作成できます。以下に例を示します。波かっことdict関数で記述が異なることに注意してください。
###### 実行コード
```
addresses = {'taro': '東京', 'jiro': '神奈川'}
print(addresses)
addresses = dict([('taro', '東京'), ('jiro', '神奈川')])
print(addresses)
```
###### 実行結果
```
{'taro': '東京', 'jiro': '神奈川'}
{'taro': '東京', 'jiro': '神奈川'}
```
1行目では波かっこにより辞書を作成しており、キーと値をコロンで区切り、波かっこで囲っています。3行目ではdict関数により辞書を作成しており、タプルとして定義されたキーと値のペアのリストを引数として与えています。記述は異なりますが、いずれの方法でも名前と住所の辞書を作成できることが確認できます。
#### 辞書の要素を取得する
辞書の要素はキーを指定することで取得できます。このとき、存在しないキーを指定するとエラーになるため注意してください。以下に例を示します。
###### 実行コード
```
addresses = {'taro': '東京', 'jiro': '神奈川'}
print('taroの住所を取得します。')
print(addresses['taro'])
print()
print('saburoの住所を取得します。')
print(addresses['saburo'])
```
###### 実行結果
```
taroの住所を取得します。
東京
saburoの住所を取得します。
Traceback (most recent call last):
File "sample.py", line 6, in <module>
print(addresses['saburo'])
KeyError: 'saburo'
```
taroの住所をキー指定により取得できること、そして存在しないキーを指定するとエラーになることが確認できます。
#### 辞書に要素を追加、更新する
辞書への要素の追加や更新は、キーを指定して値を代入することにより行えます。存在するキーを指定した場合には値が更新され、存在しない場合には新しいペアが追加されます。以下に例を示します。
###### 実行コード
```
addresses = {'taro': '東京', 'jiro': '神奈川'}
print(addresses)
addresses['taro'] = '千葉'
print(addresses)
addresses['saburo'] = '東京'
print(addresses)
```
###### 実行結果
```
{'taro': '東京', 'jiro': '神奈川'}
{'taro': '千葉', 'jiro': '神奈川'}
{'taro': '千葉', 'jiro': '神奈川', 'saburo': '東京'}
```
3行目でtaroの値が変更されていること、5行目でsaburo値が追加されていることを確認できます。
##### 辞書(複数要素)の結合
要素更新を繰り返し適用することが冗長な場合には、複数要素を辞書にまとめ、一括で更新することもできます。そのためには、updateメソッドを利用できます。以下に例を示します。
###### 実行コード
```
addresses = {'taro': '東京', 'jiro': '神奈川'}
print(addresses)
addresses.update({'taro': '千葉', 'saburo': '東京'})
print(addresses)
```
###### 実行結果
```
{'taro': '東京', 'jiro': '神奈川'}
{'taro': '千葉', 'jiro': '神奈川', 'saburo': '東京'}
```
3行目でupdateメソッドにより、taroの値が変更され、saburoの値が追加されていることを確認できます。
#### 辞書から要素を削除する
辞書の要素を削除するためには、リストと同様にdel文を利用できます。del文では削除したいキーを指定します。以下に例を示します。
###### 実行コード
```
addresses = {'taro': '東京', 'jiro': '神奈川'}
print(addresses)
del addresses['taro']
print(addresses)
```
###### 実行結果
```
{'taro': '東京', 'jiro': '神奈川'}
{'jiro': '神奈川'}
```
3行目でdelメソッドにより、taroのデータが削除されていることを確認できます。
#### 辞書に特定のキーが存在するか確認する
予期しないエラーを防ぐため、特定のキーが辞書に存在するか確認することは大切です。そのために、リストと同様in文を使用できます。以下に例を示します。
###### 実行コード
```
addresses = {'taro': '東京', 'jiro': '神奈川'}
print('taro' in addresses)
print('saburo' in addresses)
```
###### 実行結果
```
True
False
```
in文により、addressesにtaroは存在するがsaburoは存在しないことを確認できます。
#### 辞書を走査する
キーや値の一覧を確認したり、全要素を一括更新する場合には、辞書に対する反復的な処理(走査)が必要となることがあります。そのために、全キーを取得するkeys関数、全値を取得するvalues関数、そして全キーと値のペアを取得するitems関数が用意されています。以下に例を示します。
###### 実行コード
```
addresses = {'taro': '東京', 'jiro': '神奈川'}
print('名前の一覧を表示します。')
for key in addresses.keys():
print(key)
print('住所の一覧を表示します。')
for value in addresses.values():
print(value)
print('住所に都道府県を追加します。')
updated_addresses = {}
for key, value in addresses.items():
if key == '東京':
updated_addresses[key] = value + '都'
else:
updated_addresses[key] = value + '県'
print(updated_addresses)
```
###### 実行結果
```
名前の一覧を表示します。
taro
jiro
住所の一覧を表示します。
東京
神奈川
住所に都道府県を追加します。
{'taro': '東京県', 'jiro': '神奈川県'}
```
3行目でkeysメソッドにより名前の一覧を取得、表示しています。7行目では、valuesメソッドにより住所の一覧を取得、表示しています。そして、12行目ではvaluesメソッドにより名前と住所のペアの一覧を取得し、値のみを更新して新たな辞書に格納しています。
### 集合(セット)
項目の重複を削除したい場合など、集合を順序付けず扱いたいときには、セットを使うと便利です。セットは値のない辞書と考えることができ、記法や操作の共通点が多くあります。また、セットはミュータブル(変更可能)なため、リストや辞書と同様に取得や追加、変更、削除すべての操作が行えます。記法や操作方法は他のデータ構造と同様なため割愛し、セット固有の機能である集合演算について紹介します。
#### 集合演算
主な集合演算として、「和(どちらかの集合に含まれる要素の集合)」、「差(第一の集合には含まれるが第二の集合には含まれない要素の集合)」、「積(両方の集合に含まれる要素の集合)」、「対称差(どちらか一方のみに含まれている要素の集合)」が定義されています。以下に記述方法と実行コード、実行結果を示します。
###### 集合演算と記述方法
| 集合演算 | 演算子 | (演算子と等価な)メソッド |
| -------- | ------ | -------------------------- |
| 和 | \| | union |
| 差 | - | difference |
| 積 | & | intersection |
| 対称差 | ^ | symmetric_difference |
###### 実行コード
```
team_a = {'taro', 'jiro', 'saburo'}
team_b = {'shiro', 'goro', 'taro'}
print(team_a | team_b)
print(team_a.union(team_b))
print(team_a - team_b)
print(team_a.difference(team_b))
print(team_a & team_b)
print(team_a.intersection(team_b))
print(team_a ^ team_b)
print(team_a.symmetric_difference(team_b))
```
###### 実行結果
```
{'saburo', 'taro', 'goro', 'jiro', 'shiro'}
{'saburo', 'taro', 'goro', 'jiro', 'shiro'}
{'jiro', 'saburo'}
{'jiro', 'saburo'}
{'taro'}
{'taro'}
{'goro', 'shiro', 'saburo', 'jiro'}
{'goro', 'shiro', 'saburo', 'jiro'}
```
演算子あるいはメソッドにより各集合演算を行えることが確認できます。
## オブジェクトとクラス
連絡帳アプリケーションを考えましょう。このアプリケーションでは連絡先情報として入力されたデータ(ふりがなや電話番号)の形式が正しいかどうかを検査する機能が必要だとします。この機能は、データを格納した辞書変数と、各データ項目の形式を検査する関数により実現でき、連絡先ごとに辞書変数の作成と検査の関数を実行するコードを記述することになります。このとき、概念的に各項目と検査の関数は一体化しているため、コードも一体化させて項目を記述するだけで済ませたくなります。なぜなら、毎回関数の呼び出しを記述するのは冗長な上、記述し忘れによるエラー発生のリスクが高まるためです。このように、データ(変数)と命令(関数)を一体化できると便利なことがあります。一体化したデータと命令を総称してオブジェクトと呼び、人や動物など属性(名詞)と振る舞い(動詞)が一体となった物事を表します。なお、オブジェクトを使ったプログラミングはオブジェクト指向プログラミング(OOP)と呼ばれます。Pythonでは、オブジェクトを作成するためのクラスと呼ばれる仕組みが用意されています。比較のため、まずクラスを使用しない例を以下に示します。
###### 実行コード
```
import re
def validate_reading(reading):
matched_object = re.match('[\u3041-\u309F]+', reading)
if matched_object is not None:
print('ふりがなの形式に問題はありません。')
else:
print('ふりがなの形式に問題があります。')
taro = {
'name': '太郎',
'reading': 'たろう'
}
jiro = {
'name': '二郎',
'reading': 'じろう'
}
validate_reading(taro['reading'])
validate_reading(jiro['reading'])
```
###### 実行結果
```
ふりがなの形式に問題はありません。
ふりがなの形式に問題はありません。
```
1行目はライブラリ(モジュール)を利用するためのimport文で、記述することで他で定義された関数やクラスを利用することができます。4-9行目では正規表現を使った文字列形式の検査の関数を定義しています(正規表現については後のコースで詳細に説明するため説明を割愛します)。そして、12-20行目で連絡先を辞書変数として定義し、22-23行目で各辞書変数に対してふりがなの検査を行う関数を記述しています。このようにクラスを使わない場合には、各データの定義に対して関数の呼び出しを記述する必要があります。
次に、クラスを使用した例を示します。
###### 実行コード
```
import re
class Person:
def __init__(self, name, reading):
self.name = name
self.reading = reading
self.validate_reading(reading)
def validate_reading(self, reading):
matched_object = re.match('[\u3041-\u309F]+', reading)
if matched_object is not None:
print('ふりがなの形式に問題はありません。')
else:
print('ふりがなの形式に問題があります。')
taro = Person('太郎', 'たろう')
jiro = Person('二郎', 'じろう')
```
###### 実行結果
```
ふりがなの形式に問題はありません。
ふりがなの形式に問題はありません。
```
4-16行目でクラスを定義し(記述方法については後述します)、19-20行目でクラスを使用して太郎と二郎のオブジェクトを作成し、変数(taroとjiro)に格納しています。データの定義と検査が一体化しており、19-20行目で項目を記述するだけで済んでいることが確認できます。
### クラスの定義とインスタンスの作成
クラスはオブジェクトのデータ項目と関連する命令を表すテンプレートであり、データ項目は属性(プロパティ)、関連する命令(関数)はメソッドと呼ばれます。また、テンプレートに具体的なデータが肉付けされたオブジェクトの実体をインスタンスと呼びます。最も基本的なクラスでは、classキーワードを使いクラス名を定義し、\_\_init\_\_メソッドによりオブジェクトの初期化処理(データを変数に格納して検査するといったオブジェクト作成時の処理)を定義します。そして、関数呼び出しと同様に、クラス名に引数を渡すことでインスタンスを作成できます。先の例でクラス定義とインスタンスの作成方法を確認しましょう。
###### 実行コード
```
import re
class Person:
def __init__(self, name, reading):
self.validate_reading(reading)
self.name = name
self.reading = reading
def validate_reading(self, reading):
matched_object = re.match('[\u3041-\u309F]+', reading)
if matched_object is not None:
print('ふりがなの形式に問題はありません。')
else:
print('ふりがなの形式に問題があります。')
taro = Person('太郎', 'たろう')
jiro = Person('二郎', 'じろう')
print(taro)
print(jiro)
```
###### 実行結果
```
ふりがなの形式に問題はありません。
ふりがなの形式に問題はありません。
<__main__.Person object at 0x1016bb9a0>
<__main__.Person object at 0x1017bc880>
```
4行目でclassキーワードを使いクラス名を定義しています。6行目の\_\_init__はインスタンス作成時に内部的に必ず実行されるメソッドを表し、self引数は作成されるインスタンス自体を表します。また、7行目では引数として渡されたふりがなの形式が正しいかを検査するメソッドを呼び出し、8-9行目ではself引数を使ってインスタンスの属性にデータを格納しています。19-20行目では、定義したクラスに引数を渡すことで、検査済みで名前とふりがなの格納されたインスタンスを作成しています。
### インスタンスへのアクセス
インスタンスの属性値やメソッドへのアクセスは、インスタンスを格納した変数を通して行えます。以下に例を示します。
###### 実行コード
```
class Person:
def __init__(self, family_name, first_name):
self.family_name = family_name
self.first_name = first_name
self.name = family_name + first_name
def name_with_space(self):
return self.family_name + ' ' + self.first_name
taro = Person('山田', '太郎')
print(taro.name)
print(taro.name_with_space())
```
###### 実行結果
```
山田太郎
山田 太郎
```
1-9行目で名前を表す属性群と空白付きの名前を返すメソッドを定義し、12行目で太郎のインスタンスを作成しています。そして、インスタンスの格納されたtaro変数を通して13-14行目でname属性とname_with_spaceメソッドへのアクセスを行っていることが確認できます。
### 継承によるクラス定義の再利用
OOPにおける重要な要素の1つに継承があります。継承を使うことで、既存のクラスを再利用して関連するクラスをより簡単に定義することができます。再利用されるクラスを親クラス(スーパークラス)、再利用するクラスを子クラス(サブクラス)と呼び、子クラス名(親クラス名)と記述して継承を行います。子クラスには親クラスの属性やメソッドが受け継がれ、明示的に定義しなくとも、子クラスのインスタンスから親クラスの属性やメソッドにアクセスできます。また、super().メソッド名と記述することで、子クラスから親クラスのメソッドを使用することもできます。例として、先のPersonクラスに電話番号属性を追加したAddressクラスを継承を使って定義してみましょう。以下に例を示します。
###### 実行コード
```
class Person:
def __init__(self, family_name, first_name):
self.family_name = family_name
self.first_name = first_name
self.name = family_name + first_name
def name_with_space(self):
return self.family_name + ' ' + self.first_name
class Address(Person):
def __init__(self, family_name, first_name, phone_number):
super(Address, self).__init__(family_name, first_name)
self.phone_number = phone_number
def phone_number_without_hyphen(self):
return self.phone_number.replace('-', '')
taro_address = Address('山田', '太郎', '090-1234-5678')
print(taro_address.name)
print(taro_address.name_with_space())
print(taro_address.phone_number)
print(taro_address.phone_number_without_hyphen())
```
###### 実行結果
```
山田太郎
山田 太郎
090-1234-5678
09012345678
```
12行目でAddress(Person)と記述してPersonクラスを継承したAddressクラスを定義しています。14-16行目では、新たに電話番号を含めた属性の初期化を、親クラスの\_\_init\_\_メソッドをオーバーライドして、親クラスの\_\_init\_\_メソッドを再利用しながら行います。18-19行目では、新しくハイフン無しの電話番号を出力します。そして、22行目でAddressインスタンスを作成し、23-26行目で親クラスの属性やメソッドを含んだtaro_addressインスタンスの属性やメソッドにアクセスしています。このように、継承を使うことでデータや処理を追加したクラスを効率的に記述できることが確認できます。
### 多態性による簡潔なクラスアクセス
OOPにおける別の重要な要素の1つに多態性(ポリモーフィズム)があります。多態性とは、属性やメソッドが複数の型に属することを許すという性質のことで、複数のクラスの属性やメソッドへのアクセスを同様に扱うことができ、簡潔に呼び出し処理を記述することができます。特に、親クラスのメソッドと同名のメソッドを定義し、クラスの振る舞いを変更することをオーバーライドと呼びます。以下に例を示します。
###### 実行コード
```
class Person:
def __init__(self, family_name, first_name):
self.family_name = family_name
self.first_name = first_name
self.name = family_name + ' ' + first_name
def name_with_title(self):
return self.name + ' 様'
class EnglishPerson(Person):
def name_with_title(self):
return 'Mr. ' + self.name
taro = Person('山田', '太郎')
english_taro = EnglishPerson('Yamada', 'Taro')
for various_taro in [taro, english_taro]:
print(various_taro.name_with_title())
```
###### 実行結果
```
山田 太郎 様
Mr. Yamada Taro
```
1-9行目で敬称を出力するメソッドを定義したPersonクラスを定義し、12-15行目でPersonを継承して敬称を出力するメソッドをオーバーライドしたEnglishPersonクラスを定義しています。そして、18-19行目でそれぞれのインスタンスを作成し、20-21行目でname_with_titleメソッドにクラスに依らず同様の記述でアクセスできています。このとき、もしメソッド名がクラスによって異なっていれば、if文を使ってクラスごとに呼び出すメソッドを変える必要があり、記述が煩雑になってしまうでしょう。
### カプセル化による安全なクラスアクセス
OOPにおけるもう一つの重要な要素の1つにカプセル化があります。カプセル化とは、属性やメソッドへのアクセスを制限することで、不整合を引き起こすような操作をできなくさせ、エラーを防ぐ仕組みです。例えば、氏名が間に空白を含むべきにもかかわらず、氏名を外から空白を含まないように変更できた場合には、意図しない表示になってしまうでしょう。変数名に「_(アンダーバー)」を1つけることで直接的なアクセスを警告し(アクセスはできるがIDEなどで警告が出る)、2つつけると直接的なアクセスができなくなります。以下に例を示します。
###### 実行コード
```
class Person:
def __init__(self, family_name, first_name):
self.__name = family_name + ' ' + first_name
taro = Person('山田', '太郎')
print(taro.__name)
```
###### 実行結果
```
Traceback (most recent call last):
File "sample.py", line 7, in <module>
print(taro.__name)
AttributeError: 'Person' object has no attribute '__name'
```
4行目で間に空白を含んだname属性を定義しています。そして、7行目でname属性にアクセスしようとしていますが、隠蔽されているためアクセスできないことが確認できます。
#### ゲッター/セッターによる安全なアクセス
先の記述は安全ではありますが、氏名に全くアクセスできず機能として不十分です。氏名の取得は常に許可し、氏名の変更は空白を含むような場合にのみ許可するようにアクセス権を変更したほうが良いでしょう。このような場合にはゲッターとセッターを使います。ゲッターとセッターを使って先の記述を修正してみましょう。
###### 実行コード
```
import re
class Person:
def __init__(self, family_name, first_name):
self.__name = family_name + ' ' + first_name
def get_name(self):
return self.__name
def set_name(self, name):
matched_object = re.match('.+ .+', name)
if matched_object is not None:
self.__name = name
print('氏名を変更しました。')
else:
print('氏名は空白を含む必要があります。')
taro = Person('山田', '太郎')
print(taro.get_name())
taro.set_name('山田太郎')
taro.set_name('山田 太郎')
```
###### 実行結果
```
山田 太郎
氏名は空白を含む必要があります。
氏名を変更しました。
```
9-10行目でname属性の取得を許可するゲッターを定義し、12-18行目で空白を含む場合にのみname属性の変更を許可するセッターを定義しています(13行目は正規表現を使った記述です。一旦無視してください)。そして、21行目で実際にname属性の取得ができ、23-24行目で空白を含む場合にのみname属性の変更ができることを確認できます。
### クラス属性とクラスメソッド
クラス内で同一の属性やメソッドを定義したいことがあります。そのような場合には、属性やメソッドをクラス属性やクラスメソッドとして定義します。属性はクラス名の直下に定義することでクラス内で共有される1つの属性となり、クラスメソッドは@classmethodと記述することでクラスメンバのみにアクセスできるメソッドとなります。また、クラス属性やクラスメソッドは、クラス名.属性やクラス名.メソッドの書式でアクセスできます。以下に例を示します。
###### 実行コード
```
class Person:
type = 'private'
@classmethod
def is_private(cls):
return cls.type == 'private'
person = Person()
person1 = Person()
print(person.type)
print(person1.type)
print(Person.type)
print(Person.is_private())
```
###### 実行結果
```
private
private
private
True
```
3行目でクラス属性を定義し、5-7行目でタイプが個人であるかどうかを判定するクラスメソッドを定義しています。そして、10-11行目でインスタンスを作成し、12-15行目でクラス属性やクラスメソッドにアクセスしています。クラス属性の値がインスタンスに依らないこと、クラス名を使って属性やメソッドにアクセスできることが確認できます。
## まとめ
本章では、リスト、タプル、辞書、集合といった基本的なデータ構造、そしてオブジェクトとクラスの使い方を学習しました。これらは実務でアプリケーションを作る際に必ず必要となるため、十分に理解しておく必要があります。引き続き、具体的な活用シーンを思い浮かべながら、Pythonを学んでいきましょう。