如何警告类(名称)的弃用

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

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

class NewClsName:
pass


OldClsName = NewClsName

I have no idea how to mark the OldClsName as deprecated in an elegant way. Maybe I could make OldClsName a function which emits a warning (to logs) and constructs the NewClsName object from its parameters (using *args and **kvargs) but it doesn't seem elegant enough (or maybe it is?).

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

36884 次浏览

请看 warnings.warn

正如您将看到的,文档中的示例是一个弃用警告:

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

也许我可以使 OldClsName 成为一个发出警告(to )并根据其参数构造 NewClsName 对象(使用 Args and * * 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):
def __init__(self, *args, **kwargs):
warnings.warn("The 'OldClsName' class was renamed [...]",
DeprecationWarning)
NewClsName.__init__(*args, **kwargs)

使用 inspect模块为 OldClass添加占位符,然后 OldClsName is NewClsName检查将通过,类似 pylint 的行将通知此错误。

不赞成

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>

下面是一个解决方案应该满足的需求列表:

  • 已弃用类的实例化应引发警告
  • 已弃用类的子类化应引发警告
  • 支持 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的实例。

最后,通过 PEP 3119中定义的 __instancecheck____subclasscheck__实现 isinstanceissubclass检查。


测试

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

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

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

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

自 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