Python 如何区分显式传递的 None 作为内置函数中的参数

How does Python distinguish explicitly passed None as argument in built-ins

我试验了下一个代码:

>>> f = object()

# It's obvious behavior:
>>> f.foo
Traceback (most recent call last):       
  File "<stdin>", line 1, in <module>
AttributeError: 'object' object has no attribute 'foo'

# However, the next one is surprising me!
>>> getattr(f, 'foo')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'object' object has no attribute 'foo'

# And this one returns None as expected:
>>> getattr(f, 'foo', None)

然后我在PyCharmIDE中找到了这个getattr()的伪签名:

def getattr(object, name, default=None): # known special case of getattr
    """
    getattr(object, name[, default]) -> value

    Get a named attribute from an object; getattr(x, 'y') is equivalent to x.y.
    When a default argument is given, it is returned when the attribute doesn't
    exist; without it, an exception is raised in that case.
    """
    pass

我的问题是 python 如何区分这两种在内部使用 getattr()(可能还有其他函数)的场景?是否有可能完全在客户端代码中做类似的事情?

getattr 是内置的 - 因此它是在 C 中实现的 - 伪签名并不是它如何工作的精确指南。

getattr docs

getattr(object, name[ default])

Return the value of the named attribute of object. name must be a string. If the string is the name of one of the object’s attributes, the result is the value of that attribute. For example, getattr(x, 'foobar') is equivalent to x.foobar. If the named attribute does not exist, default is returned if provided, otherwise AttributeError is raised.

如果你想实现同样的尝试这个

def mygetattr(obj, attr, default):
    ret_val = defaule
    try:
        ret_val = getattr(obj, attr)
    except AttributeError:
        pass

    return ret_val

正如@scytale 所说,getattr 的伪签名与其实现并不完全对应。我已经看到尝试在纯 Python 中复制行为,看起来像这样:

class MyObject(object):
    __marker = object()

    def getvalue(key, default=__marker):
        ...
        if key is __marker:
             # no value supplied for default
             ....

换句话说,使用调用者无法轻易提供的标记值来检查是否没有给出默认值而不是 None

getattr 是用 C 实现的,所以它的实现方式与 python 中的实现方式略有不同。在 C 中有几个调用约定,getattr 使用所谓的 METH_VARARG 这意味着它期望未指定的数字位置参数作为元组传递,然后该函数检查它是否是长度为 2 的元组或 3(通过 PyArg_UnpackTuple)并相应地执行操作(当解压缩时,默认参数将在省略时成为一个 NULL 指针,它不同于任何 python 对象)。

这与 python 中的操作类似:

def mygetattr(*args):
    if len(args) != 2 and len(args) != 3:
          raise Exception
    try:
        return getattr(args[0], args[1])
    except AttributeError:
        if len(args) == 3:
            return args[2]
        raise

但通常在 python 中实际上会明确说明强制参数,然后使用 *args 来处理可选参数(即 def mygetattr(obj, key, *args):...

我会朝着与@skyking 相同的方向前进,但也允许使用这样的命名参数:

def getattr(object, name, *args, **kwargs):
    if len(args) == 1:
        # third positional argument is used as default
        default = args[1]
    elif 'default' in kwargs and len(kwargs) == 1:
        # provided as named argument
        default = kwargs['default']
    elif len(kwargs) > 0 or len(args) > 0:
        # unknown arguments
    else:
        # no 'default' was supplied

在上面的代码中,您可以在适当的地方插入处理代码和抛出的异常!

这个问题在技术上回答得很好。

但是在 IDE 中你可能会得到一个“丑陋的”函数签名提示,如下所示:

_MISSING 或您选择的任何标识符在这里可能会非常混乱。

为了提高程序员的用户体验(也可能是自动生成的代码文档),使用 overload 装饰器 (Python documentation: typing.overload) 是一个不错的选择,看到这个结果:

您会看到两个签名,因为您可能熟悉内置和其他 Python 包。

示例:

from typing import overload

_MISSING = object()


@overload
def myfunc(value):
    pass


@overload
def myfunc(value, default):
    pass


def myfunc(value, default=_MISSING):
    if default is _MISSING:
        print("Missing default on call", value)
    else:
        print(value, default)


myfunc(1, 2)  # Output: 1 2

myfunc(1)  # Output: Missing default on call 1

此重载机制仅影响 IDE(和生成的文档)。

在运行时,最后一个非修饰函数(总是需要的)在技术上将简单地覆盖 myfunc.

命名空间中之前的修饰函数

还建议输入和类型提示,但不包含在代码片段中,因为它不是问题的一部分。