如何警告类(名称)弃用

发布于 2024-12-28 17:46:14 字数 1362 浏览 2 评论 0原文

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

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

class NewClsName:
    pass

OldClsName = NewClsName

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

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

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

编辑:函数方法对我不起作用(我已经尝试过),因为该类有一些类方法(工厂方法),当 OldClsName< /code> 被定义为一个函数。以下代码不起作用:

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 不是 NewClsName 这在大多数情况下不是问题,但如果使用该库的代码编写得不好,则可能会出现问题。

我还可以创建一个虚拟的、不相关的 OldClsName 类,并实现一个构造函数以及其中所有类方法的包装器,但在我看来,这是更糟糕的解决方案。

I have renamed a python class that is part of a library. I am willing to leave a possibility to use its previous name for some time but would like to warn user that it's deprecated and will be removed in the future.

I think that to provide backward compatibility it will be enough to use an alias like that:

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?).

However, I don't know how Python standard library deprecation warnings work. I imagine that there may be some nice magic to deal with deprecation, e.g. allowing treating it as errors or silencing depending on some interpreter's command line option.

The question is: How to warn users about using an obsolete class alias (or obsolete class in general).

EDIT: The function approach doesn't work for me (I already gave it a try) because the class has some class methods (factory methods) which can't be called when the OldClsName is defined as a function. Following code won't work:

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( ... )

Because of:

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

Is inheritance my only option? To be honest, it doesn't look very clean to me - it affects class hierarchy through introduction of unnecessary derivation. Additionally, OldClsName is not NewClsName what is not an issue in most cases but may be a problem in case of poorly written code using the library.

I could also create a dummy, unrelated OldClsName class and implement a constructor as well as wrappers for all class methods in it, but it is even worse solution, in my opinion.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(7

思念满溢 2025-01-04 17:46:14

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

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?).

Yup, I think that's pretty standard practice:

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

The only tricky thing is if you have things that subclass from OldClsName - then we have to get clever. If you just need to keep access to class methods, this should do it:

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)

I haven't tested it, but that should give you the idea - __call__ will handle the normal-instantation route, __getattr__ will capture accesses to the class methods & still generate the warning, without messing with your class heirarchy.

宫墨修音 2025-01-04 17:46:14

请查看warnings.warn

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

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

Please have a look at warnings.warn.

As you'll see, the example in the documentation is a deprecation warning:

def deprecation(message):
    warnings.warn(message, DeprecationWarning, stacklevel=2)
单身狗的梦 2025-01-04 17:46:14

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

from warnings import warn

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

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

In python >= 3.6 you can easily handle warning on subclassing:

from warnings import warn

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

Overloading __new__ should allow you to warn when the old class constructor is called directly, but I haven't tested that since I don't need it right now.

凉风有信 2025-01-04 17:46:14

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

  • 已弃用类的实例化应引发警告
  • 已弃用类的子类化应引发警告
  • 支持 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__ 接收基类列表,需要用 NewClass 替换 DeprecatedClass 的实例。

最后,isinstanceissubclass 检查是通过 PEP 3119


测试

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

Here is the list of requirements a solution should satisfy:

  • Instantiation of a deprecated class should raise a warning
  • Subclassing of a deprecated class should raise a warning
  • Support isinstance and issubclass checks

Solution

This can be achieved with a custom metaclass:

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'))

Explanation

DeprecatedClassMeta.__new__ method is called not only for a class it is a metaclass of but also for every subclass of this class. That gives an opportunity to ensure that no instance of DeprecatedClass will ever be instantiated or subclassed.

Instantiation is simple. The metaclass overrides the __new__ method of DeprecatedClass to always return an instance of NewClass.

Subclassing is not much harder. DeprecatedClassMeta.__new__ receives a list of base classes and needs to replace instances of DeprecatedClass with NewClass.

Finally, the isinstance and issubclass checks are implemented via __instancecheck__ and __subclasscheck__ defined in PEP 3119.


Test

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
罪#恶を代价 2025-01-04 17:46:14

从 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

Since Python 3.7, you can provide a customization of module attribute access using __getattr__ (and __dir__). Everything is explained in PEP 562.
In the bellow example, I implemented __getattr__ and __dir__ in order to deprecate the “OldClsName” in favor of “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])

In the __getattr__ function, if a deprecated class or function name is found, a warning message is emitted, showing the source file and line number of the caller (with stacklevel=2).

In the user code, we could have:

# 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()

When the user run his script your_lib_usage.py, it will get something like this:

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

Note: the stack trace is usually written in STDERR.

To see the error warnings, you may need to add a “-W” flag in the Python command line, for instance:

python -W always your_lib_usage.py
探春 2025-01-04 17:46:14

你为什么不直接子类化呢?这样就不会破坏用户代码。

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

Why don't you just sub-class? This way no user code should be broken.

class OldClsName(NewClsName):
    def __init__(self, *args, **kwargs):
        warnings.warn("The 'OldClsName' class was renamed [...]",
                      DeprecationWarning)
        NewClsName.__init__(*args, **kwargs)
双马尾 2025-01-04 17:46:14

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

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>

Use inspect module to add placeholder for OldClass, then OldClsName is NewClsName check will pass, and a linter like pylint will inform this as error.

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())

then run 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>
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文