記述子ハウツーガイド—Pythonドキュメント

提供:Dev Guides
< PythonPython/docs/3.9/howto/descriptor
移動先:案内検索

記述子ハウツーガイド

著者
レイモンドヘッティンガー
コンタクト

記述子により、オブジェクトは属性の検索、保存、および削除をカスタマイズできます。

このガイドには、4つの主要なセクションがあります。

  1. 「入門書」は、簡単な例からゆっくりと移動し、一度に1つの機能を追加して、基本的な概要を示します。 記述子を初めて使用する場合は、ここから始めてください。
  2. 2番目のセクションは、完全で実用的な記述子の例を示しています。 すでに基本を知っている場合は、そこから始めてください。
  3. 3番目のセクションでは、記述子がどのように機能するかについての詳細なメカニズムを説明する、より技術的なチュートリアルを提供します。 ほとんどの人はこのレベルの詳細を必要としません。
  4. 最後のセクションには、Cで記述された組み込み記述子に相当する純粋なPythonがあります。 関数がどのようにバインドされたメソッドに変わるか、または classmethod()staticmethod()、 property()[X184X ]、および __ slots __

プライマー

この入門書では、考えられる最も基本的な例から始めて、新しい機能を1つずつ追加します。

簡単な例:定数を返す記述子

Tenクラスは、__get__()メソッドから定数10を常に返す記述子です。

class Ten:
    def __get__(self, obj, objtype=None):
        return 10

記述子を使用するには、記述子をクラス変数として別のクラスに格納する必要があります。

class A:
    x = 5                       # Regular class attribute
    y = Ten()                   # Descriptor instance

対話型セッションは、通常の属性ルックアップと記述子ルックアップの違いを示しています。

>>> a = A()                     # Make an instance of class A
>>> a.x                         # Normal attribute lookup
5
>>> a.y                         # Descriptor lookup
10

a.x属性ルックアップで、ドット演算子はクラスディクショナリでキーxと値5を見つけます。 a.yルックアップでは、ドット演算子は__get__メソッドによって認識される記述子インスタンスを見つけ、10を返すそのメソッドを呼び出します。

10は、クラスディクショナリにもインスタンスディクショナリにも格納されないことに注意してください。 代わりに、値10はオンデマンドで計算されます。

この例は、単純な記述子がどのように機能するかを示していますが、あまり役に立ちません。 定数を取得するには、通常の属性ルックアップの方が適しています。

次のセクションでは、より便利な動的ルックアップを作成します。


動的ルックアップ

興味深い記述子は通常、定数を返す代わりに計算を実行します。

import os

class DirectorySize:

    def __get__(self, obj, objtype=None):
        return len(os.listdir(obj.dirname))

class Directory:

    size = DirectorySize()              # Descriptor instance

    def __init__(self, dirname):
        self.dirname = dirname          # Regular instance attribute

インタラクティブセッションは、ルックアップが動的であることを示しています—毎回異なる更新された回答を計算します。

>>> s = Directory('songs')
>>> g = Directory('games')
>>> s.size                              # The songs directory has twenty files
20
>>> g.size                              # The games directory has three files
3
>>> os.remove('games/chess')            # Delete a game
>>> g.size                              # File count is automatically updated
2

この例では、記述子が計算を実行する方法を示すだけでなく、__get__()へのパラメーターの目的も明らかにします。 self パラメーターは size であり、 DirectorySize のインスタンスです。 obj パラメータは、 g または s のいずれかであり、 Directory のインスタンスです。 __get__()メソッドがターゲットディレクトリを学習できるようにするのは obj パラメータです。 objtype パラメーターはクラス Directory です。


管理属性

記述子の一般的な用途は、インスタンスデータへのアクセスの管理です。 記述子はクラスディクショナリのパブリック属性に割り当てられ、実際のデータはインスタンスディクショナリのプライベート属性として格納されます。 記述子の__get__()および__set__()メソッドは、パブリック属性にアクセスするとトリガーされます。

次の例では、 age がパブリック属性であり、 _age がプライベート属性です。 public属性にアクセスすると、記述子はルックアップまたは更新をログに記録します。

import logging

logging.basicConfig(level=logging.INFO)

class LoggedAgeAccess:

    def __get__(self, obj, objtype=None):
        value = obj._age
        logging.info('Accessing %r giving %r', 'age', value)
        return value

    def __set__(self, obj, value):
        logging.info('Updating %r to %r', 'age', value)
        obj._age = value

class Person:

    age = LoggedAgeAccess()             # Descriptor instance

    def __init__(self, name, age):
        self.name = name                # Regular instance attribute
        self.age = age                  # Calls __set__()

    def birthday(self):
        self.age += 1                   # Calls both __get__() and __set__()

対話型セッションは、管理対象属性 age へのすべてのアクセスがログに記録されているが、通常の属性 name はログに記録されていないことを示しています。

>>> mary = Person('Mary M', 30)         # The initial age update is logged
INFO:root:Updating 'age' to 30
>>> dave = Person('David D', 40)
INFO:root:Updating 'age' to 40

>>> vars(mary)                          # The actual data is in a private attribute
{'name': 'Mary M', '_age': 30}
>>> vars(dave)
{'name': 'David D', '_age': 40}

>>> mary.age                            # Access the data and log the lookup
INFO:root:Accessing 'age' giving 30
30
>>> mary.birthday()                     # Updates are logged as well
INFO:root:Accessing 'age' giving 30
INFO:root:Updating 'age' to 31

>>> dave.name                           # Regular attribute lookup isn't logged
'David D'
>>> dave.age                            # Only the managed attribute is logged
INFO:root:Accessing 'age' giving 40
40

この例の大きな問題の1つは、プライベート名 _ageLoggedAgeAccess クラスに組み込まれていることです。 つまり、各インスタンスは1つのログ属性しか持つことができず、その名前は変更できません。 次の例では、その問題を修正します。


カスタマイズされた名前

クラスが記述子を使用する場合、使用された変数名について各記述子に通知できます。

この例では、Personクラスには、 nameage の2つの記述子インスタンスがあります。 Personクラスが定義されると、 LoggedAccess__set_name__()へのコールバックが行われ、フィールド名を記録して、各記述子に独自の public_name [を与えることができます。 X182X]および private_name

import logging

logging.basicConfig(level=logging.INFO)

class LoggedAccess:

    def __set_name__(self, owner, name):
        self.public_name = name
        self.private_name = '_' + name

    def __get__(self, obj, objtype=None):
        value = getattr(obj, self.private_name)
        logging.info('Accessing %r giving %r', self.public_name, value)
        return value

    def __set__(self, obj, value):
        logging.info('Updating %r to %r', self.public_name, value)
        setattr(obj, self.private_name, value)

class Person:

    name = LoggedAccess()                # First descriptor instance
    age = LoggedAccess()                 # Second descriptor instance

    def __init__(self, name, age):
        self.name = name                 # Calls the first descriptor
        self.age = age                   # Calls the second descriptor

    def birthday(self):
        self.age += 1

対話型セッションは、Personクラスが__set_name__()を呼び出して、フィールド名が記録されることを示しています。 ここでは、 vars()を呼び出して、記述子をトリガーせずに検索します。

>>> vars(vars(Person)['name'])
{'public_name': 'name', 'private_name': '_name'}
>>> vars(vars(Person)['age'])
{'public_name': 'age', 'private_name': '_age'}

新しいクラスは、 nameage の両方へのアクセスをログに記録するようになりました。

>>> pete = Person('Peter P', 10)
INFO:root:Updating 'name' to 'Peter P'
INFO:root:Updating 'age' to 10
>>> kate = Person('Catherine C', 20)
INFO:root:Updating 'name' to 'Catherine C'
INFO:root:Updating 'age' to 20

2つの Person インスタンスには、プライベート名のみが含まれています。

>>> vars(pete)
{'_name': 'Peter P', '_age': 10}
>>> vars(kate)
{'_name': 'Catherine C', '_age': 20}

結びの考え

記述子は、__get__()__set__()、または__delete__()を定義するオブジェクトと呼ばれるものです。

オプションで、記述子は__set_name__()メソッドを持つことができます。 これは、記述子が作成されたクラスまたは割り当てられたクラス変数の名前のいずれかを知る必要がある場合にのみ使用されます。 (このメソッドが存在する場合は、クラスが記述子でなくても呼び出されます。)

記述子は、属性のルックアップ中にドット「演算子」によって呼び出されます。 vars(some_class)[descriptor_name]を使用して記述子に間接的にアクセスすると、記述子インスタンスは呼び出されずに返されます。

記述子は、クラス変数として使用される場合にのみ機能します。 インスタンスに入れても効果はありません。

記述子の主な動機は、クラス変数に格納されているオブジェクトが属性ルックアップ中に何が起こるかを制御できるようにするフックを提供することです。

従来、呼び出し側クラスはルックアップ中に何が起こるかを制御します。 記述子はその関係を逆転させ、検索されているデータが問題について発言できるようにします。

記述子は言語全体で使用されます。 これは、関数がバインドされたメソッドに変わる方法です。 classmethod()staticmethod()property()functools.cached_property()などの一般的なツールはすべて記述子として実装されています。


完全な実用例

この例では、データ破損のバグを見つけるのが難しいことで有名な場所を見つけるための実用的で強力なツールを作成します。

バリデータークラス

バリデーターは、管理された属性アクセスの記述子です。 データを保存する前に、新しい値がさまざまなタイプと範囲の制限を満たしていることを確認します。 これらの制限が満たされない場合、ソースでのデータ破損を防ぐために例外が発生します。

このValidatorクラスは、抽象基本クラスとマネージ属性記述子の両方です。

from abc import ABC, abstractmethod

class Validator(ABC):

    def __set_name__(self, owner, name):
        self.private_name = '_' + name

    def __get__(self, obj, objtype=None):
        return getattr(obj, self.private_name)

    def __set__(self, obj, value):
        self.validate(value)
        setattr(obj, self.private_name, value)

    @abstractmethod
    def validate(self, value):
        pass

カスタムバリデーターはValidatorから継承する必要があり、必要に応じてさまざまな制限をテストするためにvalidate()メソッドを提供する必要があります。


カスタムバリデーター

3つの実用的なデータ検証ユーティリティは次のとおりです。

  1. OneOfは、値が制限されたオプションのセットの1つであることを確認します。
  2. Numberは、値が int または float のいずれかであることを確認します。 オプションで、値が指定された最小値または最大値の間にあることを確認します。
  3. Stringは、値が str であることを確認します。 オプションで、指定された最小または最大の長さを検証します。 ユーザー定義の述語も検証できます。
class OneOf(Validator):

    def __init__(self, *options):
        self.options = set(options)

    def validate(self, value):
        if value not in self.options:
            raise ValueError(f'Expected {value!r} to be one of {self.options!r}')

class Number(Validator):

    def __init__(self, minvalue=None, maxvalue=None):
        self.minvalue = minvalue
        self.maxvalue = maxvalue

    def validate(self, value):
        if not isinstance(value, (int, float)):
            raise TypeError(f'Expected {value!r} to be an int or float')
        if self.minvalue is not None and value < self.minvalue:
            raise ValueError(
                f'Expected {value!r} to be at least {self.minvalue!r}'
            )
        if self.maxvalue is not None and value > self.maxvalue:
            raise ValueError(
                f'Expected {value!r} to be no more than {self.maxvalue!r}'
            )

class String(Validator):

    def __init__(self, minsize=None, maxsize=None, predicate=None):
        self.minsize = minsize
        self.maxsize = maxsize
        self.predicate = predicate

    def validate(self, value):
        if not isinstance(value, str):
            raise TypeError(f'Expected {value!r} to be an str')
        if self.minsize is not None and len(value) < self.minsize:
            raise ValueError(
                f'Expected {value!r} to be no smaller than {self.minsize!r}'
            )
        if self.maxsize is not None and len(value) > self.maxsize:
            raise ValueError(
                f'Expected {value!r} to be no bigger than {self.maxsize!r}'
            )
        if self.predicate is not None and not self.predicate(value):
            raise ValueError(
                f'Expected {self.predicate} to be true for {value!r}'
            )

実用化

データバリデーターを実際のクラスで使用する方法は次のとおりです。

class Component:

    name = String(minsize=3, maxsize=10, predicate=str.isupper)
    kind = OneOf('wood', 'metal', 'plastic')
    quantity = Number(minvalue=0)

    def __init__(self, name, kind, quantity):
        self.name = name
        self.kind = kind
        self.quantity = quantity

記述子は、無効なインスタンスが作成されるのを防ぎます。

>>> Component('Widget', 'metal', 5)      # Blocked: 'Widget' is not all uppercase
Traceback (most recent call last):
    ...
ValueError: Expected <method 'isupper' of 'str' objects> to be true for 'Widget'

>>> Component('WIDGET', 'metle', 5)      # Blocked: 'metle' is misspelled
Traceback (most recent call last):
    ...
ValueError: Expected 'metle' to be one of {'metal', 'plastic', 'wood'}

>>> Component('WIDGET', 'metal', -5)     # Blocked: -5 is negative
Traceback (most recent call last):
    ...
ValueError: Expected -5 to be at least 0
>>> Component('WIDGET', 'metal', 'V')    # Blocked: 'V' isn't a number
Traceback (most recent call last):
    ...
TypeError: Expected 'V' to be an int or float

>>> c = Component('WIDGET', 'metal', 5)  # Allowed:  The inputs are valid

テクニカルチュートリアル

以下は、記述子がどのように機能するかについてのメカニズムと詳細についてのより技術的なチュートリアルです。

概要

記述子を定義し、プロトコルを要約し、記述子がどのように呼び出されるかを示します。 オブジェクトリレーショナルマッピングがどのように機能するかを示す例を提供します。

記述子について学ぶことは、より大きなツールセットへのアクセスを提供するだけでなく、Pythonがどのように機能するかについてのより深い理解を生み出します。


定義と紹介

一般に、記述子は、記述子プロトコルのメソッドの1つを持つ属性値です。 それらのメソッドは、__get__()__set__()、および__delete__()です。 これらのメソッドのいずれかが属性に対して定義されている場合、それは記述子であると言われます。

属性アクセスのデフォルトの動作は、オブジェクトのディクショナリから属性を取得、設定、または削除することです。 たとえば、a.xには、a.__dict__['x']type(a).__dict__['x']の順に始まり、type(a)のメソッド解決順序まで続くルックアップチェーンがあります。 ルックアップされた値が記述子メソッドの1つを定義するオブジェクトである場合、Pythonはデフォルトの動作をオーバーライドし、代わりに記述子メソッドを呼び出すことがあります。 優先順位チェーンのどこでこれが発生するかは、定義された記述子メソッドによって異なります。

記述子は、強力な汎用プロトコルです。 これらは、プロパティ、メソッド、静的メソッド、クラスメソッド、および super()の背後にあるメカニズムです。 これらはPython自体全体で使用されます。 記述子は、基礎となるCコードを単純化し、日常のPythonプログラムに柔軟な新しいツールのセットを提供します。


記述子プロトコル

descr.__get__(self, obj, type=None) -> value

descr.__set__(self, obj, value) -> None

descr.__delete__(self, obj) -> None

これですべてです。 これらのメソッドのいずれかを定義すると、オブジェクトは記述子と見なされ、属性として検索されたときにデフォルトの動作をオーバーライドできます。

オブジェクトが__set__()または__delete__()を定義している場合、それはデータ記述子と見なされます。 __get__()のみを定義する記述子は、非データ記述子と呼ばれます(メソッドによく使用されますが、他の用途も可能です)。

データ記述子と非データ記述子は、インスタンスのディクショナリのエントリに関してオーバーライドが計算される方法が異なります。 インスタンスのディクショナリにデータ記述子と同じ名前のエントリがある場合、データ記述子が優先されます。 インスタンスのディクショナリに非データ記述子と同じ名前のエントリがある場合、ディクショナリエントリが優先されます。

読み取り専用のデータ記述子を作成するには、__get__()__set__()の両方を定義し、__set__()が呼び出されたときに AttributeError を発生させます。 __set__()メソッドを例外発生プレースホルダーで定義するだけで、データ記述子にすることができます。


記述子呼び出しの概要

記述子は、desc.__get__(obj)またはdesc.__get__(None, cls)を使用して直接呼び出すことができます。

ただし、記述子が属性アクセスから自動的に呼び出されるのがより一般的です。

obj.xは、objの名前空間のチェーンで属性xを検索します。 検索でインスタンス__dict__の外部に記述子が見つかった場合、その__get__()メソッドは、以下にリストされている優先順位ルールに従って呼び出されます。

呼び出しの詳細は、objがオブジェクト、クラス、またはスーパーのインスタンスであるかどうかによって異なります。


インスタンスからの呼び出し

インスタンスルックアップは、名前空間のチェーンをスキャンして、データ記述子に最高の優先度を与え、次にインスタンス変数、次に非データ記述子、次にクラス変数、最後に__getattr__()が提供されている場合にそれを与えます。

a.xの記述子が見つかった場合、desc.__get__(a, type(a))で呼び出されます。

ドットルックアップのロジックは、 object .__ getattribute __()にあります。 これは純粋なPythonの同等物です:

def object_getattribute(obj, name):
    "Emulate PyObject_GenericGetAttr() in Objects/object.c"
    null = object()
    objtype = type(obj)
    cls_var = getattr(objtype, name, null)
    descr_get = getattr(type(cls_var), '__get__', null)
    if descr_get is not null:
        if (hasattr(type(cls_var), '__set__')
            or hasattr(type(cls_var), '__delete__')):
            return descr_get(cls_var, obj, objtype)     # data descriptor
    if hasattr(obj, '__dict__') and name in vars(obj):
        return vars(obj)[name]                          # instance variable
    if descr_get is not null:
        return descr_get(cls_var, obj, objtype)         # non-data descriptor
    if cls_var is not null:
        return cls_var                                  # class variable
    raise AttributeError(name)

興味深いことに、属性ルックアップは object .__ getattribute __()を直接呼び出しません。 代わりに、ドット演算子と getattr()関数の両方が、ヘルパー関数を介して属性ルックアップを実行します。

def getattr_hook(obj, name):
    "Emulate slot_tp_getattr_hook() in Objects/typeobject.c"
    try:
        return obj.__getattribute__(name)
    except AttributeError:
        if not hasattr(type(obj), '__getattr__'):
            raise
    return type(obj).__getattr__(obj, name)             # __getattr__

したがって、__getattr__()が存在する場合、__getattribute__()AttributeError を発生させるたびに(直接または記述子呼び出しの1つで)呼び出されます。

また、ユーザーが object .__ getattribute __()を直接呼び出すと、__getattr__()フックは完全にバイパスされます。


クラスからの呼び出し

A.xなどのドットルックアップのロジックはtype.__getattribute__()にあります。 手順は object .__ getattribute __()の手順と似ていますが、インスタンスディクショナリのルックアップは、クラスのメソッド解決順序による検索に置き換えられます。

記述子が見つかると、desc.__get__(None, A)で呼び出されます。

完全なC実装は、:source: `Objects / typeobject.c`type_getattro()および_PyType_Lookup()にあります。


スーパーからの呼び出し

スーパーのドットルックアップのロジックは、 super()によって返されるオブジェクトの__getattribute__()メソッドにあります。

super(A, obj).mなどのドット付きルックアップは、Aの直後にある基本クラスBobj.__class__.__mro__で検索し、B.__dict__['m'].__get__(obj, A)を返します。 記述子でない場合、mは変更されずに返されます。

完全なC実装は、:source: `Objects / typeobject.c`super_getattro()にあります。 純粋なPythonの同等物は、 Guidoのチュートリアルにあります。


呼び出しロジックの要約

記述子のメカニズムは、オブジェクトタイプ、およびスーパー()__getattribute__()メソッドに組み込まれています。

覚えておくべき重要なポイントは次のとおりです。

  • 記述子は、__getattribute__()メソッドによって呼び出されます。
  • クラスは、オブジェクトタイプ、または super()からこの機構を継承します。
  • __getattribute__()をオーバーライドすると、すべての記述子ロジックがそのメソッド内にあるため、自動記述子呼び出しが防止されます。
  • object .__ getattribute __()type.__getattribute__()は、__get__()に対して異なる呼び出しを行います。 1つ目はインスタンスを含み、クラスを含む場合があります。 2番目はインスタンスのNoneを入力し、常にクラスを含みます。
  • データ記述子は常にインスタンスディクショナリをオーバーライドします。
  • 非データ記述子は、インスタンスディクショナリによってオーバーライドされる場合があります。


自動名前通知

記述子が割り当てられたクラス変数名を知っていることが望ましい場合があります。 新しいクラスが作成されると、 type メタクラスが新しいクラスのディクショナリをスキャンします。 エントリのいずれかが記述子であり、それらが__set_name__()を定義している場合、そのメソッドは2つの引数で呼び出されます。 owner は記述子が使用されるクラスであり、 name は記述子が割り当てられたクラス変数です。

実装の詳細は、:source: `Objects / typeobject.c`type_new()およびset_names()にあります。

更新ロジックはtype.__new__()にあるため、通知はクラスの作成時にのみ発生します。 後で記述子をクラスに追加する場合は、__set_name__()を手動で呼び出す必要があります。


ORMの例

次のコードは、データ記述子を使用してオブジェクトリレーショナルマッピングを実装する方法を示す簡略化されたスケルトンです。

基本的な考え方は、データが外部データベースに保存されることです。 Pythonインスタンスは、データベースのテーブルへのキーのみを保持します。 記述子はルックアップまたは更新を処理します。

class Field:

    def __set_name__(self, owner, name):
        self.fetch = f'SELECT {name} FROM {owner.table} WHERE {owner.key}=?;'
        self.store = f'UPDATE {owner.table} SET {name}=? WHERE {owner.key}=?;'

    def __get__(self, obj, objtype=None):
        return conn.execute(self.fetch, [obj.key]).fetchone()[0]

    def __set__(self, obj, value):
        conn.execute(self.store, [value, obj.key])
        conn.commit()

Fieldクラスを使用して、データベース内の各テーブルのスキーマを記述するモデルを定義できます。

class Movie:
    table = 'Movies'                    # Table name
    key = 'title'                       # Primary key
    director = Field()
    year = Field()

    def __init__(self, key):
        self.key = key

class Song:
    table = 'Music'
    key = 'title'
    artist = Field()
    year = Field()
    genre = Field()

    def __init__(self, key):
        self.key = key

モデルを使用するには、最初にデータベースに接続します。

>>> import sqlite3
>>> conn = sqlite3.connect('entertainment.db')

インタラクティブセッションでは、データベースからデータを取得する方法と更新する方法を示します。

>>> Movie('Star Wars').director
'George Lucas'
>>> jaws = Movie('Jaws')
>>> f'Released in {jaws.year} by {jaws.director}'
'Released in 1975 by Steven Spielberg'

>>> Song('Country Roads').artist
'John Denver'

>>> Movie('Star Wars').director = 'J.J. Abrams'
>>> Movie('Star Wars').director
'J.J. Abrams'

純粋なPythonの同等物

記述子プロトコルはシンプルで、エキサイティングな可能性を提供します。 いくつかのユースケースは非常に一般的であるため、組み込みツールに事前にパッケージ化されています。 プロパティ、バインドされたメソッド、静的メソッド、クラスメソッド、および__slots__は、すべて記述子プロトコルに基づいています。

プロパティ

property()の呼び出しは、属性へのアクセス時に関数呼び出しをトリガーするデータ記述子を構築する簡潔な方法です。 その署名は次のとおりです。

property(fget=None, fset=None, fdel=None, doc=None) -> property

ドキュメントは、管理対象属性xを定義するための一般的な使用法を示しています。

class C:
    def getx(self): return self.__x
    def setx(self, value): self.__x = value
    def delx(self): del self.__x
    x = property(getx, setx, delx, "I'm the 'x' property.")

property()が記述子プロトコルの観点からどのように実装されているかを確認するために、純粋なPythonの同等物を次に示します。

class Property:
    "Emulate PyProperty_Type() in Objects/descrobject.c"

    def __init__(self, fget=None, fset=None, fdel=None, doc=None):
        self.fget = fget
        self.fset = fset
        self.fdel = fdel
        if doc is None and fget is not None:
            doc = fget.__doc__
        self.__doc__ = doc

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        if self.fget is None:
            raise AttributeError("unreadable attribute")
        return self.fget(obj)

    def __set__(self, obj, value):
        if self.fset is None:
            raise AttributeError("can't set attribute")
        self.fset(obj, value)

    def __delete__(self, obj):
        if self.fdel is None:
            raise AttributeError("can't delete attribute")
        self.fdel(obj)

    def getter(self, fget):
        return type(self)(fget, self.fset, self.fdel, self.__doc__)

    def setter(self, fset):
        return type(self)(self.fget, fset, self.fdel, self.__doc__)

    def deleter(self, fdel):
        return type(self)(self.fget, self.fset, fdel, self.__doc__)

property()ビルトインは、ユーザーインターフェイスが属性アクセスを許可し、その後の変更でメソッドの介入が必要になる場合に役立ちます。

たとえば、スプレッドシートクラスは、Cell('b10').valueを介してセル値へのアクセスを許可する場合があります。 その後のプログラムの改善では、アクセスのたびにセルを再計算する必要があります。 ただし、プログラマーは、属性に直接アクセスする既存のクライアントコードに影響を与えたくありません。 解決策は、値属性へのアクセスをプロパティデータ記述子でラップすることです。

class Cell:
    ...

    @property
    def value(self):
        "Recalculate the cell before returning value"
        self.recalc()
        return self._value

この例では、組み込みの property()または同等のProperty()のいずれかが機能します。


関数とメソッド

Pythonのオブジェクト指向機能は、関数ベースの環境に基づいて構築されています。 非データ記述子を使用して、2つはシームレスにマージされます。

クラス辞書に格納されている関数は、呼び出されるとメソッドに変換されます。 メソッドは、オブジェクトインスタンスが他の引数の前に付加されるという点で、通常の関数とのみ異なります。 慣例により、インスタンスは self と呼ばれますが、 this またはその他の変数名と呼ばれることもあります。

メソッドは、 types.MethodType を使用して手動で作成できます。これは、次とほぼ同等です。

class MethodType:
    "Emulate PyMethod_Type in Objects/classobject.c"

    def __init__(self, func, obj):
        self.__func__ = func
        self.__self__ = obj

    def __call__(self, *args, **kwargs):
        func = self.__func__
        obj = self.__self__
        return func(obj, *args, **kwargs)

メソッドの自動作成をサポートするために、関数には、属性アクセス中にメソッドをバインドするための__get__()メソッドが含まれています。 これは、関数がインスタンスからのドットルックアップ中にバインドされたメソッドを返す非データ記述子であることを意味します。 仕組みは次のとおりです。

class Function:
    ...

    def __get__(self, obj, objtype=None):
        "Simulate func_descr_get() in Objects/funcobject.c"
        if obj is None:
            return self
        return MethodType(self, obj)

インタプリタで次のクラスを実行すると、関数記述子が実際にどのように機能するかがわかります。

class D:
    def f(self, x):
         return x

この関数には、イントロスペクションをサポートするための修飾名属性があります。

>>> D.f.__qualname__
'D.f'

クラスディクショナリを介して関数にアクセスしても、__get__()は呼び出されません。 代わりに、基になる関数オブジェクトを返すだけです。

>>> D.__dict__['f']
<function D.f at 0x00C45070>

クラスからの点線アクセスは__get__()を呼び出し、基になる関数を変更せずに返します。

>>> D.f
<function D.f at 0x00C45070>

興味深い動作は、インスタンスからのドットアクセス中に発生します。 点線のルックアップは__get__()を呼び出し、バインドされたメソッドオブジェクトを返します。

>>> d = D()
>>> d.f
<bound method D.f of <__main__.D object at 0x00B18C90>>

内部的には、バインドされたメソッドは、基になる関数とバインドされたインスタンスを格納します。

>>> d.f.__func__
<function D.f at 0x00C45070>

>>> d.f.__self__
<__main__.D object at 0x1012e1f98>

self が通常のメソッドのどこから来ているのか、または cls がクラスメソッドのどこから来ているのか疑問に思ったことがあるなら、これがそれです!


方法の種類

非データ記述子は、関数をメソッドにバインドする通常のパターンを変化させるための単純なメカニズムを提供します。

要約すると、関数には__get__()メソッドがあり、属性としてアクセスしたときにメソッドに変換できます。 非データ記述子は、obj.f(*args)呼び出しをf(obj, *args)に変換します。 cls.f(*args)を呼び出すとf(*args)になります。

このチャートは、バインディングとその2つの最も有用なバリアントをまとめたものです。

変身 オブジェクトから呼び出されます クラスから呼び出されます
関数 f(obj、* args) f(* args)
staticmethod f(* args) f(* args)
classmethod f(type(obj)、* args) f(cls、* args)


静的メソッド

静的メソッドは、変更なしで基になる関数を返します。 c.fまたはC.fのいずれかを呼び出すことは、object.__getattribute__(c, "f")またはobject.__getattribute__(C, "f")を直接検索することと同じです。 その結果、関数はオブジェクトまたはクラスのいずれからも同じようにアクセスできるようになります。

静的メソッドの適切な候補は、self変数を参照しないメソッドです。

たとえば、統計パッケージには、実験データのコンテナクラスが含まれる場合があります。 このクラスは、データに依存する平均、平均、中央値、およびその他の記述統計を計算するための通常のメソッドを提供します。 ただし、概念的には関連しているがデータに依存しない便利な関数がある場合があります。 たとえば、erf(x)は、統計作業で発生する便利な変換ルーチンですが、特定のデータセットに直接依存しません。 オブジェクトまたはクラスs.erf(1.5) --> .9332またはSample.erf(1.5) --> .9332から呼び出すことができます。

静的メソッドは変更なしで基になる関数を返すため、呼び出し例は刺激的ではありません。

class E:
    @staticmethod
    def f(x):
        return x * 10
>>> E.f(3)
30
>>> E().f(3)
30

非データ記述子プロトコルを使用すると、 staticmethod()の純粋なPythonバージョンは次のようになります。

class StaticMethod:
    "Emulate PyStaticMethod_Type() in Objects/funcobject.c"

    def __init__(self, f):
        self.f = f

    def __get__(self, obj, objtype=None):
        return self.f

クラスメソッド

静的メソッドとは異なり、クラスメソッドは、関数を呼び出す前に、引数リストのクラス参照を先頭に追加します。 この形式は、呼び出し元がオブジェクトであるかクラスであるかについて同じです。

class F:
    @classmethod
    def f(cls, x):
        return cls.__name__, x
>>> F.f(3)
('F', 3)
>>> F().f(3)
('F', 3)

この動作は、メソッドがクラス参照のみを持つ必要があり、特定のインスタンスに格納されているデータに依存しない場合に役立ちます。 クラスメソッドの用途の1つは、代替クラスコンストラクターを作成することです。 たとえば、classmethod dict.fromkeys()は、キーのリストから新しい辞書を作成します。 純粋なPythonの同等物は次のとおりです。

class Dict(dict):
    @classmethod
    def fromkeys(cls, iterable, value=None):
        "Emulate dict_fromkeys() in Objects/dictobject.c"
        d = cls()
        for key in iterable:
            d[key] = value
        return d

これで、一意のキーの新しい辞書を次のように作成できます。

>>> d = Dict.fromkeys('abracadabra')
>>> type(d) is Dict
True
>>> d
{'a': None, 'b': None, 'r': None, 'c': None, 'd': None}

非データ記述子プロトコルを使用すると、 classmethod()の純粋なPythonバージョンは次のようになります。

class ClassMethod:
    "Emulate PyClassMethod_Type() in Objects/funcobject.c"

    def __init__(self, f):
        self.f = f

    def __get__(self, obj, cls=None):
        if cls is None:
            cls = type(obj)
        if hasattr(type(self.f), '__get__'):
            return self.f.__get__(cls)
        return MethodType(self.f, cls)

hasattr(type(self.f), '__get__')のコードパスがPython3.9で追加され、 classmethod()がチェーンデコレータをサポートできるようになりました。 たとえば、classmethodとpropertyをチェーン化できます。

class G:
    @classmethod
    @property
    def __doc__(cls):
        return f'A doc for {cls.__name__!r}'
>>> G.__doc__
"A doc for 'G'"

メンバーオブジェクトと__slots__

クラスが__slots__を定義すると、インスタンス辞書がスロット値の固定長配列に置き換えられます。 いくつかの効果があるユーザーの観点から:

1. 属性の割り当てのスペルミスによるバグを即座に検出します。 __slots__で指定された属性名のみが許可されます。

class Vehicle:
    __slots__ = ('id_number', 'make', 'model')
>>> auto = Vehicle()
>>> auto.id_nubmer = 'VYE483814LQEX'
Traceback (most recent call last):
    ...
AttributeError: 'Vehicle' object has no attribute 'id_nubmer'

2. 記述子が__slots__に格納されているプライベート属性へのアクセスを管理する不変オブジェクトの作成を支援します。

class Immutable:

    __slots__ = ('_dept', '_name')          # Replace the instance dictionary

    def __init__(self, dept, name):
        self._dept = dept                   # Store to private attribute
        self._name = name                   # Store to private attribute

    @property                               # Read-only descriptor
    def dept(self):
        return self._dept

    @property
    def name(self):                         # Read-only descriptor
        return self._name
>>> mark = Immutable('Botany', 'Mark Watney')
>>> mark.dept
'Botany'
>>> mark.dept = 'Space Pirate'
Traceback (most recent call last):
    ...
AttributeError: can't set attribute
>>> mark.location = 'Mars'
Traceback (most recent call last):
    ...
AttributeError: 'Immutable' object has no attribute 'location'

3. メモリを節約します。 64ビットのLinuxビルドでは、2つの属性を持つインスタンスは、__slots__がある場合は48バイト、ない場合は152バイトかかります。 このフライウェイトデザインパターンは、多数のインスタンスが作成される場合にのみ問題になる可能性があります。

4. functools.cached_property()のように、正しく機能するためにインスタンスディクショナリを必要とするツールをブロックします。

from functools import cached_property

class CP:
    __slots__ = ()                          # Eliminates the instance dict

    @cached_property                        # Requires an instance dict
    def pi(self):
        return 4 * sum((-1.0)**n / (2.0*n + 1.0)
                       for n in reversed(range(100_000)))
>>> CP().pi
Traceback (most recent call last):
  ...
TypeError: No '__dict__' attribute on 'CP' instance to cache 'pi' property.

__slots__の正確なドロップイン純粋なPythonバージョンを作成することはできません。これは、C構造体に直接アクセスし、オブジェクトのメモリ割り当てを制御する必要があるためです。 ただし、スロットの実際のC構造体がプライベート_slotvaluesリストによってエミュレートされる、ほぼ忠実なシミュレーションを構築できます。 そのプライベート構造の読み取りと書き込みは、メンバー記述子によって管理されます。

null = object()

class Member:

    def __init__(self, name, clsname, offset):
        'Emulate PyMemberDef in Include/structmember.h'
        # Also see descr_new() in Objects/descrobject.c
        self.name = name
        self.clsname = clsname
        self.offset = offset

    def __get__(self, obj, objtype=None):
        'Emulate member_get() in Objects/descrobject.c'
        # Also see PyMember_GetOne() in Python/structmember.c
        value = obj._slotvalues[self.offset]
        if value is null:
            raise AttributeError(self.name)
        return value

    def __set__(self, obj, value):
        'Emulate member_set() in Objects/descrobject.c'
        obj._slotvalues[self.offset] = value

    def __delete__(self, obj):
        'Emulate member_delete() in Objects/descrobject.c'
        value = obj._slotvalues[self.offset]
        if value is null:
            raise AttributeError(self.name)
        obj._slotvalues[self.offset] = null

    def __repr__(self):
        'Emulate member_repr() in Objects/descrobject.c'
        return f'<Member {self.name!r} of {self.clsname!r}>'

type.__new__()メソッドは、クラス変数へのメンバーオブジェクトの追加を処理します。

class Type(type):
    'Simulate how the type metaclass adds member objects for slots'

    def __new__(mcls, clsname, bases, mapping):
        'Emuluate type_new() in Objects/typeobject.c'
        # type_new() calls PyTypeReady() which calls add_methods()
        slot_names = mapping.get('slot_names', [])
        for offset, name in enumerate(slot_names):
            mapping[name] = Member(name, clsname, offset)
        return type.__new__(mcls, clsname, bases, mapping)

object .__ new __()メソッドは、インスタンスディクショナリの代わりにスロットを持つインスタンスの作成を処理します。 純粋なPythonでの大まかなシミュレーションは次のとおりです。

class Object:
    'Simulate how object.__new__() allocates memory for __slots__'

    def __new__(cls, *args):
        'Emulate object_new() in Objects/typeobject.c'
        inst = super().__new__(cls)
        if hasattr(cls, 'slot_names'):
            empty_slots = [null] * len(cls.slot_names)
            object.__setattr__(inst, '_slotvalues', empty_slots)
        return inst

    def __setattr__(self, name, value):
        'Emulate _PyObject_GenericSetAttrWithDict() Objects/object.c'
        cls = type(self)
        if hasattr(cls, 'slot_names') and name not in cls.slot_names:
            raise AttributeError(
                f'{type(self).__name__!r} object has no attribute {name!r}'
            )
        super().__setattr__(name, value)

    def __delattr__(self, name):
        'Emulate _PyObject_GenericSetAttrWithDict() Objects/object.c'
        cls = type(self)
        if hasattr(cls, 'slot_names') and name not in cls.slot_names:
            raise AttributeError(
                f'{type(self).__name__!r} object has no attribute {name!r}'
            )
        super().__delattr__(name)

実際のクラスでシミュレーションを使用するには、Objectから継承し、 metaclassTypeに設定します。

class H(Object, metaclass=Type):
    'Instance variables stored in slots'

    slot_names = ['x', 'y']

    def __init__(self, x, y):
        self.x = x
        self.y = y

この時点で、メタクラスは x および y のメンバーオブジェクトをロードしました。

>>> from pprint import pp
>>> pp(dict(vars(H)))
{'__module__': '__main__',
 '__doc__': 'Instance variables stored in slots',
 'slot_names': ['x', 'y'],
 '__init__': <function H.__init__ at 0x7fb5d302f9d0>,
 'x': <Member 'x' of 'H'>,
 'y': <Member 'y' of 'H'>}

インスタンスが作成されると、属性が格納されるslot_valuesリストがあります。

>>> h = H(10, 20)
>>> vars(h)
{'_slotvalues': [10, 20]}
>>> h.x = 55
>>> vars(h)
{'_slotvalues': [55, 20]}

属性のスペルが間違っているか割り当てられていない場合、例外が発生します。

>>> h.xz
Traceback (most recent call last):
    ...
AttributeError: 'H' object has no attribute 'xz'