PythonProgrammingPython開発者

**Python**のデスクリプターがクラス作成中に自動的に割り当てられた属性名と含まれるクラスを受け取るプロトコルメソッドは何であり、デスクリプターが宣言後にクラスに付与されるときに生じる制限は何ですか?

Hintsage AIアシスタントで面接を突破

質問への回答。

歴史。

Python 3.6以前は、属性名を知る必要があるデスクリプターは、カスタムメタクラスや手動クラスデコレータを使用してクラス辞書をスキャンし、名前を注入する必要がありました。このアプローチは冗長で、エラーを引き起こしやすく、複雑な階層内でメタクラスの競合を生じさせました。 PEP 487 は、Python 3.6で__set_name__プロトコルを導入し、インタープリタがデスクリプターに自動的に通知できるようにして、このボイラープレートを排除しました。

問題。

デスクリプターインスタンスは、クラス本体の実行中に作成されますが、その瞬間にはバインドされている変数名や存在するクラスについての固有の知識を持っていません。この情報は、有意義なエラーメッセージを生成したり、ORMシステムにフィールドを登録したり、シリアライズ化スキーマを構築するために必要です。外部通知がない場合、デスクリプターは匿名のままで、開発者は属性名を文字列引数として繰り返す必要があり、DRY原則に違反します。

解決策。

type.__new__がクラスを構築する際に、__prepare__によって返される名前空間マッピングを反復処理します。__set_name__メソッドを持つ各値に対して、インタープリタはvalue.__set_name__(owner_class, attribute_name)を呼び出します。このメソッドは構築中のクラスと属性文字列を受け取り、デスクリプターがこのメタデータを保存できるようにします。しかし、デスクリプターがクラス作成プロセスが完了した後にクラス属性に割り当てられた場合(モンキーパッチ)、__set_name__は自動的に呼び出されません。なぜなら、タイプ機構はもうアクティブではないからです。

class TrackedDescriptor: def __set_name__(self, owner, name): self.owner = owner self.name = name def __get__(self, instance, owner): if instance is None: return self return f"{self.owner.__name__}.{self.name}" class Model: field = TrackedDescriptor() # Model.field.name == 'field' # Model.field.owner == Model

生活からの状況

文脈。

構成管理ライブラリを開発している際に、環境変数を表すデスクリプターが必要でした。値が欠落または無効な場合、エラーはクラス内の正確な属性名を指定する必要がありました(例えば、Config.database_url is required)、単なる一般的なメッセージではありません。

問題。

最初は、ユーザーは手動で名前を指定する必要がありました: database_url = EnvVar('database_url')。これにより、文字列リテラルと変数名が分岐するリファクタリング中にバグが発生し、暗号的なランタイムエラーを引き起こしました。

検討された異なる解決策:

メタクラスの注入。 attrsを検査して各デスクリプターにattr.set_name(name)を呼び出すConfigMetaを実装しました。これではうまく機能しましたが、すべてのユーザークラスが当社のメタクラスを継承することを強要し、abc.ABCMetaのような他のライブラリを使用している場合の互換性を壊しました。また、メタクラスに不慣れなユーザーに対する認知的負担も増えました。

クラスデコレーターのパッチ適用。 クラス作成後にcls.__dict__を反復処理し、名前をパッチした@configデコレーターを作成しました。これによりメタクラスの競合を避けましたが、オプトインであり、デコレーターを忘れるとデスクリプターが壊れる結果となりました。また、クラス作成後に実行されるため、デスクリプターは__init_subclass__フック中に名前を使用できず、内省能力が制限されました。

__set_name__プロトコル。 EnvVarデスクリプターに__set_name__を追加しました。これによりユーザーコードの変更は不要で、クラス定義中に自動的に機能し、デスクリプターが__init_subclass__が完了する前にその名前を知ることができ、初期検証を可能にしました。

選択された解決策。

__set_name__を採用しました。これはユーザーにとってコストゼロの抽象化を提供し、Pythonのネイティブデータモデルと統合されました。メタクラスの衝突問題を完全に排除しました。

結果。

APIは宣言的になりました: database_url = EnvVar()。リファクタリングツールは属性を安全に名前変更でき、エラーメッセージは正確なままでした。コードベースは150行のメタクラスのボイラープレートを削減し、構成キーの不一致に関するバグ報告が少なくなりました。

候補者が見失うことが多い点

__set_name__はクラス作成ライフサイクルのどのタイミングで呼び出されるか。

クラス本体の実行が終了し、名前空間辞書がポピュレーションされる直後にtype.__new__によって呼び出されますが、親クラスの__init_subclass__が呼び出される前です。このタイミングは重要であり、デスクリプターがサブクラスが初期化される前に状態を最終決定できるからです。既に作成されたクラスに属性を追加する際には、トリガーされません(例えば、setattr(MyClass, 'new_attr', descriptor()))、なぜならクラス作成プロトコルは終了しているからです。この区別を理解することは、動的なクラス操作にとって重要です。

なぜ__set_name__はオーナークラスと名前の両方を引数として受け取り、selfから推測しないのか。

デスクリプターインスタンスはクラスとは独立に存在し、クラス作成前にインスタンス化され、理論的には複数のクラスに割り当てることができます(ただし珍しい)。owner引数は、デスクリプターがどの特定のクラスに割り当てられたかを知るために必要です。もしもデスクリプターが基底クラスで定義されている場合、__set_name__は基底クラスと共に呼び出されます。サブクラスで新しいインスタンスによってオーバーライドされた場合、サブクラスと共に呼び出されます。これにより、基底クラスと派生クラス間の相互汚染なしにクラス毎のレジストリが可能となります。

__set_name____set__および__get__デスクリプタプロトコルメソッドとどのように相互作用するか。

__set_name__は単なる初期化フックであり、属性アクセスプロトコル(__get__/__set__)には参加しません。しかし、操作に必要なコンテキストを提供することにより、それらのメソッドが正しく機能することを可能にします。一般的な誤解は、デスクリプターがオーバーライドされないサブクラスによって継承された場合に再び__set_name__が呼び出されると考えることです。同じデスクリプターインスタンスが再利用されるため、__set_name__は再度呼び出されません。したがって、クラス毎の状態を追跡するデスクリプターは、継承に関しては__init_subclass__を使用するか、__get__ownerを確認する必要があります。単独で__set_name__に依存するのではなく、サブクラス特有のロジックを処理するために。