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__
メソッドを持つことを保証しました。