1. はじめに
この問題集は、Pythonの基礎を習得した後、次の段階へ進みたい人のサポートをすることが目的です。
また、競技プログラミングとは異なり、複雑なアルゴリズムの問題ではなく「可読性の最大化」に焦点を当てた問題が中心となります。
2. 問題
"""
No.2 list/tuple問題の解消
以下のget_first関数を、次の要件を満たすように改修してください。
* 引数valuesは、list型だけではなくtuple型、str型、listを継承したMyList型など、
values[0]で値を取得できるような全ての型を受け入れる。
* 関数の機能は変えないこと。
つまり、valuesの長さが0であればNoneを、1以上であれば先頭要素を返す。
"""
class MyList(list):
pass
def get_first[T](values: list[T]) -> T | None:
if values:
return values[0]
else:
return None
3. 解答例
from collections.abc import Sequence
def get_first[T](values: Sequence[T]) -> T | None:
if values:
return values[0]
else:
return None
4. 採点基準
- TypeHintが適切であること
5. 解説
5.1 TypeHint
改修前のget_first関数では、引数valuesにtuple型を受け取ることができませんでした。
def get_first[T](values: list[T]) -> T | None:
if values:
return values[0]
else:
return None
get_first(values=(0, 1, 2)) # invalid type
valuesの型を修正し、tuple型などを受け取れるようにするのがこの問題の意図です。
しかし、次のようなTypeHintは望ましくありません。
def get_first[T](values: list[T] | tuple[T, ...] | str | MyList[T]) -> T | None:
...
Union型を使った書き方ではTypeHintが際限なく長くなってしまいます。
そこで、別の方法を考えることにしましょう。
まずは要求の内容を観察します。
すると、valuesは次の条件を満たせばよいということがわかります。
-
values[0]のように角括弧アクセスができる -
len(values)で長さを取得できる
そして、それぞれ内部では次のメソッドが呼び出されています。
- 角括弧アクセス →
__getitem__ - 長さの取得 →
__len__
つまりこの状況では、この2つのメソッドをサポートするクラスならvaluesに指定しても正常に処理できます。
では、valuesの型はどう書くべきでしょうか?
ここで登場するのがcollections.abcモジュールです。
この中で、__getitem__, __len__メソッドをサポートするクラスとしてSequenceが定義されています。
collections.abc --- コンテナの抽象基底クラス - docs.python.org
Add an UML class diagram to the collections.abc module documentation - github.com
ただし見ての通り継承関係が複雑なため、理解するのには骨が折れます。
そこで今回は、「list/tupleのどちらでもよい場合、TypeHintにSequenceを使う」ということだけ覚えればOKです。
以上の議論から、valuesの型はSequence[T]とするのが適切です。
6. 補足
if文の真理値判定について捕捉します。
次のif文ではvaluesの長さが0以外のときTrueと判定されます。
if values:
実はこの短いコードの裏には、巧妙なトリックが仕掛けられています。
まずは一般的な状況で考えましょう。
次のif文で条件が成立するパターンを正確に挙げてみてください。
if something: # somethingの型は不明とする
正解は次のいずれかのパターンです。
-
somethingに__bool__メソッドが定義されていて、Trueを返す -
somethingに__bool__メソッドが定義されていない、かつ__len__メソッドが定義されていて0以外の値を返す -
somethingに__bool__,__len__メソッドが定義されていない
実際に試してみましょう。
-
__bool__メソッドが定義されているかで2パターン -
__bool__メソッドの戻り値(False,True)で2パターン -
__len__メソッドが定義されているかで2パターン -
__len__メソッドの戻り値(0, 1)で2パターン
が考えられます。
(ただし、メソッドが定義されていない場合は戻り値を考えられないので重複が生じます。)
| No. |
__bool__の定義 |
__bool__の戻り値 |
__len__の定義 |
__len__の戻り値 |
|---|---|---|---|---|
| 1 | なし | False |
なし | 0 |
| 2 | なし | False |
なし | 1 |
| 3 | なし | False |
あり | 0 |
| 4 | なし | False |
あり | 1 |
| 5 | なし | True |
なし | 0 |
| 6 | なし | True |
なし | 1 |
| 7 | なし | True |
あり | 0 |
| 8 | なし | True |
あり | 1 |
| 9 | あり | False |
なし | 0 |
| 10 | あり | False |
なし | 1 |
| 11 | あり | False |
あり | 0 |
| 12 | あり | False |
あり | 1 |
| 13 | あり | True |
なし | 0 |
| 14 | あり | True |
なし | 1 |
| 15 | あり | True |
あり | 0 |
| 16 | あり | True |
あり | 1 |
Pythonコード
class Something1:
pass
class Something2:
pass
class Something3:
def __len__(self) -> int:
return 0
class Something4:
def __len__(self) -> int:
return 1
class Something5:
pass
class Something6:
pass
class Something7:
def __len__(self) -> int:
return 0
class Something8:
def __len__(self) -> int:
return 1
class Something9:
def __bool__(self) -> bool:
return False
class Something10:
def __bool__(self) -> bool:
return False
class Something11:
def __bool__(self) -> bool:
return False
def __len__(self) -> int:
return 0
class Something12:
def __bool__(self) -> bool:
return False
def __len__(self) -> int:
return 1
class Something13:
def __bool__(self) -> bool:
return True
class Something14:
def __bool__(self) -> bool:
return True
class Something15:
def __bool__(self) -> bool:
return True
def __len__(self) -> int:
return 0
class Something16:
def __bool__(self) -> bool:
return True
def __len__(self) -> int:
return 1
somethings = {
1: Something1(),
2: Something2(),
3: Something3(),
4: Something4(),
5: Something5(),
6: Something6(),
7: Something7(),
8: Something8(),
9: Something9(),
10: Something10(),
11: Something11(),
12: Something12(),
13: Something13(),
14: Something14(),
15: Something15(),
16: Something16(),
}
for number, something in somethings.items():
if something:
print(f'something{number}: True')
something1: True
something2: True
something4: True
something5: True
something6: True
something8: True
something13: True
something14: True
something15: True
something16: True
それぞれ次のパターンに該当します。
-
somethingに__bool__メソッドが定義されていて、Trueを返す
→ No.13, 14, 15, 16 -
somethingに__bool__メソッドが定義されていない、かつ__len__メソッドが定義されていて0以外の値を返す
→ No.4, 8 -
somethingに__bool__,__len__メソッドが定義されていない
→ No.1, 2, 5, 6
要するに、__bool__の戻り値が最優先、次いで__len__の戻り値が採用され、どちらも定義されていない場合はTrue判定となります。
そのため、次のように「__bool__と__getitem__をサポートするが、__len__はサポートしない」クラスをvaluesに指定してもエラーは発生しません。
def get_first[T](values) -> T | None:
if values:
return values[0]
else:
return None
class SupportsBoolAndGetItem:
def __init__(self, values: list) -> None:
self._values = values
def __bool__(self) -> bool:
return bool(self._values)
def __getitem__(self, index: int):
return self._values[index]
values = SupportsBoolAndGetItem([0, 1, 2])
assert bool(values) is True
assert values[0] == 0
try:
_ = len(values)
except TypeError as e:
print(e)
print(get_first(values=values)) # no error
object of type 'SupportsBoolAndGetItem' has no len()
0
このSupportsBoolAndGetItemクラスは__len__メソッドをサポートしていないため、collections.abcモジュールのSequenceクラスの子クラスではありません。
このことから、get_firstのvalues引数に指定できる型はSequenceより広いことがわかります。
しかしここまで話を広げると流石に収集つかなくなるので、今回は要件に「valuesの長さが~」と書くことで、valuesが__len__メソッドを持つことを保証しました。
