65

ライブラリの一部であるPythonクラスの名前を変更しました。以前の名前を使用する可能性はしばらく残しておきますが、廃止され、将来削除されることをユーザーに警告したいと思います。

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

class NewClsName:
    pass

OldClsName = NewClsName

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

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

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

編集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クラスを作成し、その中のすべてのクラスメソッドのコンストラクターとラッパーを実装することもできますが、私の意見では、それはさらに悪い解決策です。

4

7 に答える 7

41

たぶん、OldClsNameを(ログに)警告を発し、そのパラメーターから(*argsと**kvargsを使用して)NewClsNameオブジェクトを構築する関数にすることができますが、十分にエレガントではないようです(またはそうですか?)。

うん、それはかなり標準的な方法だと思う:

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__し、クラスメソッドへのアクセスをキャプチャし、クラス階層を台無しにすることなく警告を生成します。

于 2012-01-25T18:55:58.103 に答える
20

ご覧くださいwarnings.warn

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

def deprecation(message):
    warnings.warn(message, DeprecationWarning, stacklevel=2)
于 2012-01-25T18:54:21.013 に答える
9

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

  • 非推奨のクラスをインスタンス化すると、警告が表示されます
  • 非推奨のクラスをサブクラス化すると、警告が表示されます
  • サポートisinstanceissubclassチェック

解決

これは、カスタムメタクラスを使用して実現できます。

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これにより、のインスタンスがインスタンス化またはサブクラス化されないようにする機会が与えられます。

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

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

最後に、isinstanceチェックissubclassPEP 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
于 2018-08-30T00:50:17.673 に答える
9

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

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

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

于 2018-11-23T16:23:07.290 に答える
5

サブクラスにしてみませんか?このようにして、ユーザーコードが壊れてはなりません。

class OldClsName(NewClsName):
    def __init__(self, *args, **kwargs):
        warnings.warn("The 'OldClsName' class was renamed [...]",
                      DeprecationWarning)
        NewClsName.__init__(*args, **kwargs)
于 2014-09-30T22:10:10.157 に答える
5

__getattr__Python 3.7以降、 (および)を使用してモジュール属性アクセスのカスタマイズを提供できます__dir__。すべてはPEP562で説明されています。次の例では、「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__.__name__ + " is created in use_new_class")


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


if __name__ == '__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
于 2019-03-13T10:27:21.290 に答える
2

inspectモジュールを使用してのプレースホルダーを追加するとOldClassOldClsName 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>
于 2018-02-25T22:31:43.217 に答える