web-dev-qa-db-ja.com

クラス(名前)の廃止について警告する方法

pythonクラスの名前を変更しました。しばらくの間、以前の名前を使用する可能性を残しますが、廃止予定で削除されることをユーザーに警告します将来は。

下位互換性を提供するには、次のようなエイリアスを使用するだけで十分だと思います。

class NewClsName:
    pass

OldClsName = NewClsName

OldClsNameを非推奨としてエレガントな方法でマークする方法がわかりません。多分私はOldClsNameを(ログへの)警告を発し、そのパラメータからNewClsNameオブジェクトを構築する関数にすることができます(*args**kvargsを使用して)十分にエレガントに見えません(またはおそらくそれはそうですか?)。

ただし、Python標準ライブラリの非推奨の警告がどのように機能するかはわかりません。たとえば、一部のインタープリターに応じてエラーとして処理したりサイレンシングしたりできるようにするなど、非推奨に対処するための素晴らしい魔法があると思います。コマンドラインオプション。

問題は、廃止されたクラスエイリアス(または廃止されたクラス一般)の使用についてユーザーに警告する方法です。

[〜#〜] edit [〜#〜]:クラスにはいくつかのクラスメソッド(ファクトリメソッド)があるため、関数アプローチは私には機能しません(すでに試してみました)。 OldClsNameが関数として定義されているときに呼び出されます。次のコードは機能しません:

class NewClsName(object):
    @classmethod
    def CreateVariant1( cls, ... ):
        pass

    @classmethod
    def CreateVariant2( cls, ... ):
        pass

def OldClsName(*args, **kwargs):
    warnings.warn("The 'OldClsName' class was renamed [...]",
                  DeprecationWarning )
    return NewClsName(*args, **kwargs)

OldClsName.CreateVariant1( ... )

のため:

AttributeError: 'function' object has no attribute 'CreateVariant1'

継承が唯一の選択肢ですか?正直なところ、私にはあまりきれいに見えません-不要な派生の導入を通じてクラス階層に影響を与えます。さらに、OldClsName is not NewClsNameはほとんどの場合問題ではありませんが、ライブラリを使用して不十分に記述されたコードの場合は問題である可能性があります。

私はダミーの無関係なOldClsNameクラスを作成し、その中にすべてのクラスメソッドのラッパーと同様にコンストラクターを実装することもできましたが、私の意見ではさらに悪い解決策です。

48
Dariusz Walczak

OldClsNameを(ログへの)警告を発し、パラメーターからNewClsNameオブジェクトを構築する関数にすることができます(* argsと** kvargsを使用)。

うん、私はそれがかなり標準的な習慣だと思います:

def OldClsName(*args, **kwargs):
    from warnings import warn
    warn("get with the program!")
    return NewClsName(*args, **kwargs)

唯一注意が必要なのは、OldClsNameからサブクラス化するものがある場合です。その場合、賢くする必要があります。クラスメソッドへのアクセスを維持する必要があるだけの場合は、次のようにする必要があります。

class DeprecationHelper(object):
    def __init__(self, new_target):
        self.new_target = new_target

    def _warn(self):
        from warnings import warn
        warn("Get with the program!")

    def __call__(self, *args, **kwargs):
        self._warn()
        return self.new_target(*args, **kwargs)

    def __getattr__(self, attr):
        self._warn()
        return getattr(self.new_target, attr)

OldClsName = DeprecationHelper(NewClsName)

私はそれをテストしていませんが、それはあなたにアイデアを与えるでしょう-__call__は通常のインスタンス化ルートを処理します__getattr__はクラスメソッドへのアクセスをキャプチャしますが、クラス階層をいじることなく警告を生成します。

32
AdamKG

warnings.warn

ご覧のとおり、ドキュメントの例は非推奨の警告です。

def deprecation(message):
    warnings.warn(message, DeprecationWarning, stacklevel=2)
13
jcollado

なぜサブクラスにしないのですか?このようにして、ユーザーコードを壊すことはできません。

class OldClsName(NewClsName):
    def __init__(self, *args, **kwargs):
        warnings.warn("The 'OldClsName' class was renamed [...]",
                      DeprecationWarning)
        NewClsName.__init__(*args, **kwargs)
4
David Zwicker

ソリューションが満たすべき要件のリストは次のとおりです。

  • 非推奨のクラスをインスタンス化すると警告が発生するはずです
  • 非推奨のクラスをサブクラス化すると警告が発生するはずです
  • isinstanceおよびissubclassチェックをサポート

解決

これは、カスタムメタクラスで実現できます。

class DeprecatedClassMeta(type):
    def __new__(cls, name, bases, classdict, *args, **kwargs):
        alias = classdict.get('_DeprecatedClassMeta__alias')

        if alias is not None:
            def new(cls, *args, **kwargs):
                alias = getattr(cls, '_DeprecatedClassMeta__alias')

                if alias is not None:
                    warn("{} has been renamed to {}, the alias will be "
                         "removed in the future".format(cls.__name__,
                             alias.__name__), DeprecationWarning, stacklevel=2)

                return alias(*args, **kwargs)

            classdict['__new__'] = new
            classdict['_DeprecatedClassMeta__alias'] = alias

        fixed_bases = []

        for b in bases:
            alias = getattr(b, '_DeprecatedClassMeta__alias', None)

            if alias is not None:
                warn("{} has been renamed to {}, the alias will be "
                     "removed in the future".format(b.__name__,
                         alias.__name__), DeprecationWarning, stacklevel=2)

            # Avoid duplicate base classes.
            b = alias or b
            if b not in fixed_bases:
                fixed_bases.append(b)

        fixed_bases = Tuple(fixed_bases)

        return super().__new__(cls, name, fixed_bases, classdict,
                               *args, **kwargs)

    def __instancecheck__(cls, instance):
        return any(cls.__subclasscheck__(c)
            for c in {type(instance), instance.__class__})

    def __subclasscheck__(cls, subclass):
        if subclass is cls:
            return True
        else:
            return issubclass(subclass, getattr(cls,
                              '_DeprecatedClassMeta__alias'))

説明

DeprecatedClassMeta.__new__メソッドは、メタクラスであるクラスだけでなく、このクラスのすべてのサブクラスに対しても呼び出されます。これにより、DeprecatedClassのインスタンスがインスタンス化またはサブクラス化されないようにすることができます。

インスタンス化は簡単です。メタクラスはDeprecatedClass__new__ メソッドをオーバーライドして、常にNewClassのインスタンスを返します。

サブクラス化はそれほど難しくありません。 DeprecatedClassMeta.__new__は基本クラスのリストを受け取り、DeprecatedClassのインスタンスをNewClassで置き換える必要があります。

最後に、isinstanceおよびissubclassチェックは、 PEP 3119 で定義されている__instancecheck__および__subclasscheck__を介して実装されます。


テスト

class NewClass:
    foo = 1


class NewClassSubclass(NewClass):
    pass


class DeprecatedClass(metaclass=DeprecatedClassMeta):
    _DeprecatedClassMeta__alias = NewClass


class DeprecatedClassSubclass(DeprecatedClass):
    foo = 2


class DeprecatedClassSubSubclass(DeprecatedClassSubclass):
    foo = 3


assert issubclass(DeprecatedClass, DeprecatedClass)
assert issubclass(DeprecatedClassSubclass, DeprecatedClass)
assert issubclass(DeprecatedClassSubSubclass, DeprecatedClass)
assert issubclass(NewClass, DeprecatedClass)
assert issubclass(NewClassSubclass, DeprecatedClass)

assert issubclass(DeprecatedClassSubclass, NewClass)
assert issubclass(DeprecatedClassSubSubclass, NewClass)

assert isinstance(DeprecatedClass(), DeprecatedClass)
assert isinstance(DeprecatedClassSubclass(), DeprecatedClass)
assert isinstance(DeprecatedClassSubSubclass(), DeprecatedClass)
assert isinstance(NewClass(), DeprecatedClass)
assert isinstance(NewClassSubclass(), DeprecatedClass)

assert isinstance(DeprecatedClassSubclass(), NewClass)
assert isinstance(DeprecatedClassSubSubclass(), NewClass)

assert NewClass().foo == 1
assert DeprecatedClass().foo == 1
assert DeprecatedClassSubclass().foo == 2
assert DeprecatedClassSubSubclass().foo == 3
2
Kentzo

python> = 3.6では、サブクラス化に関する警告を簡単に処理できます。

class OldClassName(NewClassName):
    def __init_subclass__(self):
        warn("Class has been renamed NewClassName", DeprecationWarning, 2)

オーバーロード__new__を使用すると、古いクラスコンストラクターが直接呼び出されたときに警告を表示できますが、今は必要ないため、テストしていません。

2
Bitdancer

inspectモジュールを使用してOldClassのプレースホルダーを追加すると、OldClsName is NewClsNameチェックがパスし、pylintのようなリンターがこれをエラーとして通知します。

deprecate.py

import inspect
import warnings
from functools import wraps

def renamed(old_name):
    """Return decorator for renamed callable.

    Args:
        old_name (str): This name will still accessible,
            but call it will result a warn.

    Returns:
        decorator: this will do the setting about `old_name`
            in the caller's module namespace.
    """

    def _wrap(obj):
        assert callable(obj)

        def _warn():
            warnings.warn('Renamed: {} -> {}'
                        .format(old_name, obj.__name__),
                        DeprecationWarning, stacklevel=3)

        def _wrap_with_warn(func, is_inspect):
            @wraps(func)
            def _func(*args, **kwargs):
                if is_inspect:
                    # XXX: If use another name to call,
                    # you will not get the warning.
                    frame = inspect.currentframe().f_back
                    code = inspect.getframeinfo(frame).code_context
                    if [line for line in code
                            if old_name in line]:
                        _warn()
                else:
                    _warn()
                return func(*args, **kwargs)
            return _func

        # Make old name available.
        frame = inspect.currentframe().f_back
        assert old_name not in frame.f_globals, (
            'Name already in use.', old_name)

        if inspect.isclass(obj):
            obj.__init__ = _wrap_with_warn(obj.__init__, True)
            placeholder = obj
        else:
            placeholder = _wrap_with_warn(obj, False)

        frame.f_globals[old_name] = placeholder

        return obj

    return _wrap

test.py

from __future__ import print_function

from deprecate import renamed


@renamed('test1_old')
def test1():
    return 'test1'


@renamed('Test2_old')
class Test2(object):
    pass

    def __init__(self):
        self.data = 'test2_data'

    def method(self):
        return self.data

# pylint: disable=undefined-variable
# If not use this inline pylint option, 
# there will be E0602 for each old name.
assert(test1() == test1_old())
assert(Test2_old is Test2)
print('# Call new name')
print(Test2())
print('# Call old name')
print(Test2_old())

次にpython -W all test.pyを実行します:

test.py:22: DeprecationWarning: Renamed: test1_old -> test1
# Call new name
<__main__.Test2 object at 0x0000000007A147B8>
# Call old name
test.py:27: DeprecationWarning: Renamed: Test2_old -> Test2
<__main__.Test2 object at 0x0000000007A147B8>
1
Nate Scarlet

Python 3.7以降、__getattr__(および__dir__)を使用してモジュール属性アクセスのカスタマイズを提供できます。すべては PEP 562 で説明されています。以下の例では、「NewClsNam」を優先して「OldClsName」を廃止するために、__getattr__および__dir__を実装しました。

# your_lib.py

import warnings

__all__ = ["NewClsName"]

DEPRECATED_NAMES = [('OldClsName', 'NewClsName')]


class NewClsName:
    @classmethod
    def create_variant1(cls):
        return cls()


def __getattr__(name):
    for old_name, new_name in DEPRECATED_NAMES:
        if name == old_name:
            warnings.warn(f"The '{old_name}' class or function is renamed '{new_name}'",
                          DeprecationWarning,
                          stacklevel=2)
            return globals()[new_name]
    raise AttributeError(f"module {__name__} has no attribute {name}")


def __dir__():
    return sorted(__all__ + [names[0] for names in DEPRECATED_NAMES])

__getattr__関数で、非推奨のクラスまたは関数名が見つかった場合、呼び出し元のソースファイルと行番号(stacklevel=2付き)を示す警告メッセージが発行されます。

ユーザーコードでは、次のようになります。

# your_lib_usage.py
from your_lib import NewClsName
from your_lib import OldClsName


def use_new_class():
    obj = NewClsName.create_variant1()
    print(obj.__class__.__+ " is created in use_new_class")


def use_old_class():
    obj = OldClsName.create_variant1()
    print(obj.__class__.__+ " is created in use_old_class")


if __== '__main__':
    use_new_class()
    use_old_class()

ユーザーがスクリプトyour_lib_usage.pyを実行すると、次のようになります。

NewClsName is created in use_new_class
NewClsName is created in use_old_class
/path/to/your_lib_usage.py:3: DeprecationWarning: The 'OldClsName' class or function is renamed 'NewClsName'
  from your_lib import OldClsName

注:スタックトレースは通常STDERRに書き込まれます。

エラー警告を表示するには、たとえば、Pythonコマンドラインに「-W」フラグを追加する必要がある場合があります。

python -W always your_lib_usage.py
0
Laurent LAPORTE