はじめに
Pythonに再入門しました。
いざ、DDDをベースにコードを書き進めると、当時は見えなかった面白さが目についたので、基礎的なポイントをご紹介します。
Pythonのattributeは外部から追加できる
サンプルのコードは以下の通りです。
class Employee:
def __init__(self, name, entry_date):
self.__name = name
self.__entry_date = entry_date
@property
def name(self):
return self.__name
@property
def entry_date(self):
return self.__entry_date
emp = Employee("John Doe", "2001-04-01")
print("Employee Name:", emp.name, "/Entry Date:", emp.entry_date)
emp.retire_date = "2024-03-31"
print("Retire Date:", emp.retire_date)
ポイントは最後から2行目。コンストラクタに存在しない「何か」に値を代入しています。
このスクリプトを実行すると、以下のような出力が得られます。
% python sample1.py
Employee Name: John Doe /Entry Date: 2001-04-01
Retire Date: 2024-03-31
なんと、エラーは発生せずにEmployee
インスタンスのretire_date
に値が設定されています。
この辺は、古いバージョンのPHPを触っていた人ならば何となく理解できるポイントだと思いますが、動的に属性(PHPでいうところのプロパティ)を増やせることは、珍しいことではないのかもしれません。
PHPではバージョン8.2から動的にプロパティを追加することが非推奨となりました。
https://wiki.php.net/rfc/deprecate_dynamic_properties
Pythonでも同じように「外部から属性を追加できないように制御したい!」という場合は、以下のように__slots__
節を記述することで、外部から属性を追加できないようになります。
class Employee:
def __init__(self, name, entry_date):
self.__name = name
self.__entry_date = entry_date
+ __slots__ = ['__name', '__entry_date']
@property
def name(self):
return self.__name
@property
def entry_date(self):
return self.__entry_date
emp = Employee("John Doe", "2001-04-01")
print("Employee Name:", emp.name, "/Entry Date:", emp.entry_date)
emp.retire_date = "2024-03-31"
print("Retire Date:", emp.retire_date)
このコードを実行すると、以下のような結果になります。
% python sample1.py
Employee Name: John Doe /Entry Date: 2001-04-01
Traceback (most recent call last):
File "/Users/yoda/python/sample1.py", line 19, in <module>
emp.retire_date = "2024-03-31"
^^^^^^^^^^^^^^^
AttributeError: 'Employee' object has no attribute 'retire_date'
きちんとretire_date
は存在しない属性として、エラーを返すようになりました。
Pythonのattributeは代入するまで「存在しない」
続いてのサンプルコードはこちら。
class Employee:
def __init__(self, name, entry_date):
self.__name = name
self.__entry_date = entry_date
__slots__ = ['__name', '__entry_date', '__retire_date']
@property
def name(self):
return self.__name
@property
def entry_date(self):
return self.__entry_date
@property
def retire_date(self):
return getattr(self, '__retire_date', None)
@entry_date.setter
def entry_date(self, value):
if self.__retire_date is not None:
if value > self.__retire_date:
raise ValueError("Entry date cannot be after retire date.")
self.__entry_date = value
@retire_date.setter
def retire_date(self, value):
if value < self.__entry_date:
raise ValueError("Retire date cannot be before entry date.")
self.__retire_date = value
emp = Employee("John Doe", "2001-04-01")
emp.retire_date = "2024-03-31"
emp.entry_date = "2000-01-01"
print("Employee Name:", emp.name, "/Entry Date:", emp.entry_date, "/Retire Date:", emp.retire_date)
emp2 = Employee("Jane Smith", "2005-06-15")
emp2.entry_date = "2004-01-01"
print("Employee Name:", emp2.name, "/Entry Date:", emp2.entry_date, "/Retire Date:", emp2.retire_date)
期待しているのは、retire_date
は初期状態で設定されておらず、インスタンス生成後にセッター経由で値を設定する必要があること、entry_date
の値を再設定する場合、retire_date
設定値との相関をチェックし、日付が逆転しないよう制御されていること、という仕様にしています。
なので、上記コードのemp.entry_date = "2000-01-01"
と、emp2.entry_date = "2004-01-01"
は、いずれもエラーが発生しないことを望みます。
それでは、実際にコードを実行すると、以下の通りになります。
% python sample2.py
Employee Name: John Doe /Entry Date: 2000-01-01 /Retire Date: None
Traceback (most recent call last):
File "/Users/yoda/python/sample2.py", line 42, in <module>
emp2.entry_date = "2004-01-01"
^^^^^^^^^^^^^^^
File "/Users/yoda/python/sample2.py", line 22, in entry_date
if self.__retire_date is not None:
^^^^^^^^^^^^^^^^^^
AttributeError: 'Employee' object has no attribute '_Employee__retire_date'. Did you mean: '_Employee__entry_date'?
emp
は期待通りの結果となりましたが、emp2
でエラーが発生します。
エラーメッセージの通り、Pythonのattributeは、例え__slots__
節で定義していても、代入するまでは存在しない扱いとなるのです。
よって、期待通り動作させるためには、コンストラクタで明示的にNone
を代入するよう変更します。
class Employee:
def __init__(self, name, entry_date):
self.__name = name
self.__entry_date = entry_date
+ self.__retire_date = None
__slots__ = ['__name', '__entry_date', '__retire_date']
@property
def name(self):
return self.__name
@property
def entry_date(self):
return self.__entry_date
@property
def retire_date(self):
return getattr(self, '__retire_date', None)
@entry_date.setter
def entry_date(self, value):
if self.__retire_date is not None:
if value > self.__retire_date:
raise ValueError("Entry date cannot be after retire date.")
self.__entry_date = value
@retire_date.setter
def retire_date(self, value):
if value < self.__entry_date:
raise ValueError("Retire date cannot be before entry date.")
self.__retire_date = value
emp = Employee("John Doe", "2001-04-01")
emp.retire_date = "2024-03-31"
emp.entry_date = "2000-01-01"
print("Employee Name:", emp.name, "/Entry Date:", emp.entry_date, "/Retire Date:", emp.retire_date)
emp2 = Employee("Jane Smith", "2005-06-15")
emp2.entry_date = "2004-01-01"
print("Employee Name:", emp2.name, "/Entry Date:", emp2.entry_date, "/Retire Date:", emp2.retire_date)
上記修正後の実行結果は以下の通り。エラーは発生しなくなります。
% python sample2.py
Employee Name: John Doe /Entry Date: 2000-01-01 /Retire Date: None
Employee Name: Jane Smith /Entry Date: 2004-01-01 /Retire Date: None
まとめ
静的型付けされていない言語は、ところどころに独特な挙動があるので、触っていて面白い一方で、オブジェクト指向開発する場合は予期せぬ落とし穴があちこちにありそうです。
新しい言語を学ぶ際は、イミュータブルなクラス(言語によってはレコード)を実際に一つ作ってみて、それを破壊できるかどうか試行錯誤してみると面白いかもしれません。