【问题标题】:How to warn about class (name) deprecation如何警告类(名称)弃用
【发布时间】:2012-01-25 18:51:11
【问题描述】:

我已经重命名了作为库一部分的 python 类。我愿意在一段时间内保留使用它以前的名称的可能性,但想警告用户它已被弃用,将来会被删除。

我认为为了提供向后兼容性,使用这样的别名就足够了:

class NewClsName:
    pass

OldClsName = NewClsName

我不知道如何以优雅的方式将OldClsName 标记为已弃用。也许我可以让OldClsName 成为一个函数,它会发出警告(对日志)并根据其参数构造NewClsName 对象(使用*args**kvargs),但它看起来不够优雅(或者它可能是?)。

但是,我不知道 Python 标准库弃用警告是如何工作的。我想可能有一些很好的魔法来处理弃用,例如允许根据某些解释器的命令行选项将其视为错误或静音。

问题是:如何警告用户使用过时的类别名(或一般的过时类)。

编辑:函数方法对我不起作用(我已经尝试过了),因为该类有一些类方法(工厂方法)在@987654327 时无法调用@ 被定义为一个函数。以下代码不起作用:

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 类,并为其中的所有类方法实现构造函数和包装器,但在我看来,这是更糟糕的解决方案。

【问题讨论】:

  • 我们通常不应该为此使用内置注释@DeprecationWarning吗?

标签: python class backwards-compatibility deprecation-warning


【解决方案1】:

也许我可以让 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__ 将捕获对类方法的访问并仍然生成警告,而不会弄乱你的类层次结构。

【讨论】:

  • 支持继承也应该很简单——写一个class OldClsName(NewClsName): # and overload __new__
  • +1 用于确认使用此解决方案并提及warnings 模块。不幸的是,功能解决方案对我不起作用(请参阅已编辑的问题)。也许您还有其他一些干净的解决方案? :D
  • 更新了一个使用包装对象的示例,该对象可以代理对新类的调用和属性访问。
  • 它不会像isinstance(NewClsName(), OldClsName)issubclass(NewClsName, OldClsName)一样通过检查。
【解决方案2】:

请查看warnings.warn

如您所见,文档中的示例是弃用警告:

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

【讨论】:

  • 这是关于弃用一个类,而不是弃用一个方法/函数。
  • 这给了我我关心的行为,即 PyCharm 将删除以warnings.warn 作为第一行的方法的任何用法。因此,通过将它放在一个类的__init__/__init_subclass 中,当您尝试使用它时会得到删除线。但是,如果您将其包装在这样的方法中,它将无法正常工作。
【解决方案3】:

以下是解决方案应满足的要求列表:

  • 不推荐使用的类的实例化应引发警告
  • 不推荐使用的类的子类化应引发警告
  • 支持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 的实例永远不会被实例化或子类化的机会。

实例化很简单。元类覆盖DeprecatedClass__new__ 方法以始终返回NewClass 的实例。

子类化并不难。 DeprecatedClassMeta.__new__ 接收一个基类列表,需要用NewClass 替换DeprecatedClass 的实例。

最后,isinstanceissubclass 检查是通过在 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

【讨论】:

  • 优秀的答案,应该被 IMO 接受。
【解决方案4】:

在 python >= 3.6 中,您可以轻松处理子类化警告:

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

重载__new__ 应该允许您在直接调用旧的类构造函数时发出警告,但我没有测试过,因为我现在不需要它。

【讨论】:

  • 这将始终发出警告,无论是否正在使用已弃用的类。
【解决方案5】:

你为什么不只是子类?这样就不会破坏任何用户代码。

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

【讨论】:

  • isinstance 也会检查子类,所以如果这是你试图暗示的,这不应该破坏任何东西。
  • isinstance 检查确实会失败。考虑一下,您有执行 isinstance(obj, OldClsName) 的旧代码和实例化 obj = NewClsName() 然后 isinstance(obj, OldClsName) == False 的新代码并且您的代码中断:哎呀。
  • @DylanYoung 别忘了issubclass()。你可以使用__instancecheck____subclasscheck__ 来表示。
【解决方案6】:

从 Python 3.7 开始,您可以使用__getattr__(和__dir__)提供对模块属性访问的自定义。一切都在PEP 562 中进行了解释。 在下面的示例中,我实现了 __getattr____dir__ 以弃用“OldClsName”以支持“NewClsNam”:

# 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

【讨论】:

    【解决方案7】:

    使用inspect 模块为OldClass 添加占位符,然后OldClsName is NewClsName 检查将通过,并且像 pylint 这样的 linter 会将此通知为错误。

    弃用.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>
    

    【讨论】:

    • 如果将坏名用作类实例的变量名,这将失败;例如在其他课程中有self.Test2_old = Test2()
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2021-01-05
    • 2015-06-27
    • 1970-01-01
    • 2021-01-09
    • 2020-04-07
    • 2020-03-29
    相关资源
    最近更新 更多