首页 > 解决方案 > 为什么我的 Python 装饰器有时是“str”类型的?

问题描述

背景:

我正在编写一个装饰器函数,val_limiter它限制子函数传递的参数的值。为了彻底(以及将来可能使用),我想让我的函数能够接受数字字符串,同时能够适当地处理非数字字符串。因此,例如,看下面的代码:

@val_limiter('5')
def test(val)
    return val

test(3)运行的返回值为3。该函数成功转换'5'5. 当我尝试执行以下操作时会出现问题:

@val_limiter('foo')
def test(val)
    return val

这引发TypeError: 'str' object is not callable@val_limiter('foo'). 我想处理类似的异常test(3),它会返回一条错误消息(即:'The argument must be a number (you tried 'foo')


问题:

当我跑步时type(val_limiter('5')),我得到function,但当我跑步时type(val_limiter('foo')),我得到str。为什么会这样?处理此异常的最佳方法是什么?


源代码:

import operator
from functools import wraps

def val_limiter(val=0, limit=max, equal=True, force=False):

    val_types = [int, float, str]
    if type(val) not in val_types:
        val = int(val)
    if type(val) == str:
        try:
            val = int(val)
        except ValueError:
            return f'First argument must be a number (tried {val})'

    def decorator(fn):
        @wraps(fn)
        def wrapper(*args, **kwargs):
            ops = {
                "<": operator.lt,
                ">": operator.gt
            }
            for i in args:
                i = int(i)
                modifier = 0
                op = '<'
                try:
                    eq_string = 'is'
                    if not equal:
                        eq_string = 'cannot exceed'
                        modifier = 1 if limit == min else -1
                    if limit == min: op = '>'
                    if ops[op](val + modifier, i):
                        if force:
                            return val + modifier
                        raise ValueError
                except ValueError:
                    return f'The {limit.__name__}imum value accepted {eq_string} {val} (tried {i})'
            return fn(int(*args), **kwargs)
        return wrapper
    return decorator

*编辑:这是错误的完整追溯:

Traceback (most recent call last):
  File "/home/ec2-user/environment/playground/test.py", line 79, in test_bb_arg_1_is_str_fail
    @val_limiter('foo')
TypeError: 'str' object is not callable

*编辑:我目前正在通过一个测试文件运行它。这是失败测试的运行摘录:

import unittest

class TestValLimiter(unittest.TestCase):

    def test_bb_arg_1_is_str_fail(self):
        @val_limiter('foo')
        def test(val):
            return val
        self.assertEqual(test(3), 'First argument must be a number (tried foo)')

if __name__ == '__main__':
    unittest.main()

标签: pythontype-conversionpython-decorators

解决方案


问题在这里:

if type(val) == str:
    try:
        val = int(val)
    except ValueError:
        return f'First argument must be a number (tried {val})'

当您通过'foo'时,它(正确)被检测为 a str,并尝试将其转换为int. 但'foo'不能解析为int,所以它返回错误信息。

装饰器只是语法糖:

@val_limiter('foo')
def test(val)
    return val

大致相当于:

def test(val)
    return val
test = val_limiter('foo')(test)

因此,对于这种'foo'情况,最后的等效行变为:

test = 'First argument must be a number (tried foo)'(test)

它试图“调用” a str,就好像它是一个函数一样。

主要的收获是当你不能对它们做任何有用的事情时,你不应该捕获异常。许多入门的编程课程都教如何捕获异常,但不教什么时候这样做(实际上,教你如何做的练习经常做错了,将异常转换为prints 或returns 没有意义,除非调用者显式检查返回的类型和值,这是应该避免的异常)。

在这种情况下,您可能需要更有用的错误消息,因此如果您使用更好的错误消息引发新异常(您正在发出更友好的消息,但仍有一个错误您没有上下文来处理),而不是默默地返回垃圾(返回除了函数之外的任何东西都是返回垃圾)。所以在这种情况下,你可能会改变:

return f'First argument must be a number (tried {val})'

至:

raise ValueError(f'First argument must be a number (tried {val!r})')

这会引发一个不能意外用作函数的异常(使用非错误返回的方式)。


推荐阅读