Python数据模型和内置函数有什么关系?

What is the relationship between the Python data model and built-in functions?

当我在 Stack Overflow 上阅读 Python 个答案时,我继续直接看到一些人 telling users to use the data model's special methods or attributes

然后我看到相互矛盾的建议(有时来自我自己)说不要这样做,而是直接使用内置函数和运算符。

这是为什么? "dunder"的特殊"dunder"方法和Pythondata model and builtin functions的属性有什么关系?

我什么时候应该使用特殊名称?

Python 数据模型和内置函数之间的关系是什么?

  • 内置函数和运算符使用底层数据模型方法或属性。
  • 内置函数和运算符具有更优雅的行为,并且通常更向前兼容。
  • 数据模型的特殊方法在语义上是非public接口。
  • 内置函数和语言运算符专门用作通过特殊方法实现的行为的用户界面。

因此,您应该尽可能使用内置函数和运算符,而不是数据模型的特殊方法和属性。

内部语义 API 比 public 接口更容易发生变化。虽然 Python 实际上并没有考虑任何 "private" 并暴露了内部结构,但这并不意味着滥用该访问权限是个好主意。这样做有以下风险:

  • 升级 Python 可执行文件或切换到 Python 的其他实现(如 PyPy、IronPython、Jython 或其他一些不可预见的实施。)
  • 您的同事可能会低估您的语言能力和责任心,并将其视为代码异味,让您和您的其他代码受到更严格的审查。
  • 内置函数很容易拦截行为。使用特殊方法直接限制了您 Python 自省和调试的能力。

深入

内置函数和运算符调用特殊方法并使用 Python 数据模型中的特殊属性。它们是隐藏对象内部的可读和可维护的单板。一般来说,用户应该使用语言中提供的内置函数和运算符,而不是直接调用特殊方法或使用特殊属性。

内置函数和运算符也可以具有回退或比更原始的数据模型特殊方法更优雅的行为。例如:

  • next(obj, default) 允许您提供默认值而不是在迭代器用完时引发 StopIteration,而 obj.__next__() 则不会。
  • str(obj)obj.__str__() 不可用时回退到 obj.__repr__() - 而直接调用 obj.__str__() 会引发属性错误。
  • obj != other 在 Python 3 中没有 __ne__ 时回退到 not obj == other - 调用 obj.__ne__(other) 不会利用这一点。

(如果有必要或需要,内置函数也可以很容易地在模块的全局范围或 builtins 模块中被遮盖,以进一步自定义行为。)

将内置函数和运算符映射到数据模型

这是内置函数和运算符到它们使用的各自特殊方法和属性的映射,并附有注释 return - 请注意,通常的规则是内置函数通常映射到同名的特殊方法,但这不够一致,不能保证在下面给出这张地图:

builtins/     special methods/
operators  -> datamodel               NOTES (fb == fallback)

repr(obj)     obj.__repr__()          provides fb behavior for str
str(obj)      obj.__str__()           fb to __repr__ if no __str__
bytes(obj)    obj.__bytes__()         Python 3 only
unicode(obj)  obj.__unicode__()       Python 2 only
format(obj)   obj.__format__()        format spec optional.
hash(obj)     obj.__hash__()
bool(obj)     obj.__bool__()          Python 3, fb to __len__
bool(obj)     obj.__nonzero__()       Python 2, fb to __len__
dir(obj)      obj.__dir__()
vars(obj)     obj.__dict__            does not include __slots__
type(obj)     obj.__class__           type actually bypasses __class__ -
                                      overriding __class__ will not affect type
help(obj)     obj.__doc__             help uses more than just __doc__
len(obj)      obj.__len__()           provides fb behavior for bool
iter(obj)     obj.__iter__()          fb to __getitem__ w/ indexes from 0 on
next(obj)     obj.__next__()          Python 3
next(obj)     obj.next()              Python 2
reversed(obj) obj.__reversed__()      fb to __len__ and __getitem__
other in obj  obj.__contains__(other) fb to __iter__ then __getitem__
obj == other  obj.__eq__(other)
obj != other  obj.__ne__(other)       fb to not obj.__eq__(other) in Python 3
obj < other   obj.__lt__(other)       get >, >=, <= with @functools.total_ordering
complex(obj)  obj.__complex__()
int(obj)      obj.__int__()
float(obj)    obj.__float__()
round(obj)    obj.__round__()
abs(obj)      obj.__abs__()

operator 模块有 length_hint,如果 __len__ 没有实现,它有一个通过相应特殊方法实现的回退:

length_hint(obj)  obj.__length_hint__() 

点查找

虚线查找是上下文相关的。如果没有特殊的方法实现,首先在 class 层次结构中查找数据描述符(如属性和槽),然后在实例 __dict__ 中查找(例如变量),然后在 class 层次结构中查找非-数据描述符(如方法)。特殊方法实现以下行为:

obj.attr      obj.__getattr__('attr')       provides fb if dotted lookup fails
obj.attr      obj.__getattribute__('attr')  preempts dotted lookup
obj.attr = _  obj.__setattr__('attr', _)    preempts dotted lookup
del obj.attr  obj.__delattr__('attr')       preempts dotted lookup

描述符

描述符有点高级 - 请随意跳过这些条目,稍后再回来 - 回想一下描述符实例位于 class 层次结构中(如方法、槽和属性)。数据描述符实现 __set____delete__:

obj.attr        descriptor.__get__(obj, type(obj)) 
obj.attr = val  descriptor.__set__(obj, val)
del obj.attr    descriptor.__delete__(obj)

当实例化(定义)class 时,如果任何描述符有它来通知描述符其属性名称,则调用以下描述符方法 __set_name__。 (这是 Python 3.6 中的新内容。) cls 与上面的 type(obj) 相同, 'attr' 代表属性名称:

class cls:
    @descriptor_type
    def attr(self): pass # -> descriptor.__set_name__(cls, 'attr') 

项目(下标符号)

下标符号也是上下文相关的:

obj[name]         -> obj.__getitem__(name)
obj[name] = item  -> obj.__setitem__(name, item)
del obj[name]     -> obj.__delitem__(name)

如果 __getitem__ 找不到密钥,则调用 dict__missing__ 的子 classes 的特殊情况:

obj[name]         -> obj.__missing__(name)  

运算符

还有+, -, *, @, /, //, %, divmod(), pow(), **, <<, >>, &, ^, |运算符的特殊方法,例如:

obj + other   ->  obj.__add__(other), fallback to other.__radd__(obj)
obj | other   ->  obj.__or__(other), fallback to other.__ror__(obj)

和用于增强赋值的就地运算符,+=, -=, *=, @=, /=, //=, %=, **=, <<=, >>=, &=, ^=, |=,例如:

obj += other  ->  obj.__iadd__(other)
obj |= other  ->  obj.__ior__(other)

(如果未定义这些就地运算符,Python 退回到,例如,对于 obj += otherobj = obj + other

和一元运算:

+obj          ->  obj.__pos__()
-obj          ->  obj.__neg__()
~obj          ->  obj.__invert__()

上下文管理器

上下文管理器定义 __enter__,它在进入代码块时被调用(它的 return 值,通常是 self,别名为 as),以及 __exit__],保证在离开代码块时调用,并带有异常信息。

with obj as enters_return_value: #->  enters_return_value = obj.__enter__()
    raise Exception('message')
                                 #->  obj.__exit__(Exception, 
                                 #->               Exception('message'), 
                                 #->               traceback_object)

如果 __exit__ 得到一个异常然后 return 是一个假值,它会在离开方法时重新引发它。

如果没有例外,__exit__ 将获得 None 这三个参数,并且 return 值无意义:

with obj:           #->  obj.__enter__()
    pass
                    #->  obj.__exit__(None, None, None)

一些元class特殊方法

类似地,classes 可以有支持抽象基础的特殊方法(来自它们的元classes:classes:

isinstance(obj, cls) -> cls.__instancecheck__(obj)
issubclass(sub, cls) -> cls.__subclasscheck__(sub)

一个重要的收获是,虽然像 nextbool 这样的内置函数在 Python 2 和 3 之间没有变化,但底层实现名称 改变。

因此使用内置函数也提供了更多的向前兼容性。

我什么时候应该使用特殊名称?

在 Python 中,以下划线开头的名称在语义上是非 public 用户名称。下划线是创作者的表达方式,"hands-off, don't touch."

这不仅仅是文化上的,也是Python对API的对待。当包的 __init__.py 使用 import * 从子包提供 API 时,如果子包不提供 __all__,它将排除以下划线开头的名称。子包的 __name__ 也将被排除。

IDE 自动完成工具在将以下划线开头的名称视为非 public 时混杂在一起。但是,我非常感谢没有看到 __init____new____repr____str____eq__ 等(也没有任何用户创建非 public 接口)当我键入对象名称和句点时。

因此我断言:

特殊的 "dunder" 方法不是 public 接口的一部分。避免直接使用它们。

那么什么时候使用它们呢?

主要用例是实现您自己的自定义对象或内置对象的子class。

尽量只在绝对必要时使用它们。以下是一些示例:

在函数上使用 __name__ 特殊属性或 classes

当我们装饰一个函数时,我们通常会在 return 中得到一个包装函数,它隐藏了有关该函数的有用信息。我们会使用 @wraps(fn) 装饰器来确保我们不会丢失该信息,但是如果我们需要函数的名称,我们需要直接使用 __name__ 属性:

from functools import wraps

def decorate(fn): 
    @wraps(fn)
    def decorated(*args, **kwargs):
        print('calling fn,', fn.__name__) # exception to the rule
        return fn(*args, **kwargs)
    return decorated

类似地,当我在方法中需要对象的名称class时(例如,用于__repr__):

def get_class_name(self):
    return type(self).__name__
          # ^          # ^- must use __name__, no builtin e.g. name()
          # use type, not .__class__

使用特殊属性编写自定义 classes 或 subclassed 内置函数

当我们想要定义自定义行为时,我们必须使用数据模型名称。

这是有道理的,因为我们是实施者,这些属性对我们来说不是私有的。

class Foo(object):
    # required to here to implement == for instances:
    def __eq__(self, other):      
        # but we still use == for the values:
        return self.value == other.value
    # required to here to implement != for instances:
    def __ne__(self, other): # docs recommend for Python 2.
        # use the higher level of abstraction here:
        return not self == other  

然而,即使在这种情况下,我们也不使用 self.value.__eq__(other.value)not self.__eq__(other)(请参阅我的 answer here 以证明后者会导致意外行为。)相反,我们应该使用更高层次的抽象。

我们需要使用特殊方法名称的另一点是当我们处于子级的实现中并希望委托给父级时。例如:

class NoisyFoo(Foo):
    def __eq__(self, other):
        print('checking for equality')
        # required here to call the parent's method
        return super(NoisyFoo, self).__eq__(other) 

结论

特殊方法允许用户实现对象内部的接口。

尽可能使用内置函数和运算符。仅在没有记录 public API.

的情况下使用特殊方法

我将展示一些您显然没有想到的用法,评论您展示的示例,并反对您自己的回答中的隐私声明。


我同意你自己的回答,例如应该使用 len(a),而不是 a.__len__()。我会这样说:len 存在所以我们可以使用它,__len__ 存在所以 len 可以使用它。或者无论如何这在内部确实有效,因为 len(a) 实际上可以 更快 ,至少对于列表和字符串来说是这样:

>>> timeit('len(a)', 'a = [1,2,3]', number=10**8)
4.22549770486512
>>> timeit('a.__len__()', 'a = [1,2,3]', number=10**8)
7.957335462257106

>>> timeit('len(s)', 's = "abc"', number=10**8)
4.1480574509332655
>>> timeit('s.__len__()', 's = "abc"', number=10**8)
8.01780160432645

但除了在我自己的 类 中定义这些方法以供内置函数和运算符使用外,我偶尔也会按如下方式使用它们:

假设我需要为某些函数提供过滤功能,并且我想使用集合 s 作为过滤器。我不会创建额外的函数 lambda x: x in sdef f(x): return x in s。不,我已经有了一个可以使用的完美函数:集合的 __contains__ 方法。它更简单、更直接。甚至更快,如下所示(忽略我在这里将其保存为f,那只是为了这个时序演示):

>>> timeit('f(2); f(4)', 's = {1, 2, 3}; f = s.__contains__', number=10**8)
6.473739433621368
>>> timeit('f(2); f(4)', 's = {1, 2, 3}; f = lambda x: x in s', number=10**8)
19.940786514456924
>>> timeit('f(2); f(4)', 's = {1, 2, 3}\ndef f(x): return x in s', number=10**8)
20.445680107760325

所以虽然我不直接调用s.__contains__(x)这样的魔术方法,但我偶尔会传递它们some_function_needing_a_filter(s.__contains__)。我认为这非常好,比 lambda/def 替代方案更好。


我对你展示的例子的看法:

  • Example 1:问如何获取列表的大小,他回答items.__len__()。甚至没有任何道理。我的结论是:那是错误的。应该是 len(items).
  • Example 2:先提到d[key] = value!然后添加 d.__setitem__(key, value) 和推理 "if your keyboard is missing the square bracket keys",这很少适用,我怀疑这是认真的。我认为这只是最后一点的第一步,提到这就是我们如何在我们自己的 类 中支持方括号语法。这又回到了使用方括号的建议。
  • Example 3:建议 obj.__dict__。不好,就像 __len__ 的例子。但我怀疑他只是不知道vars(obj),我能理解,因为vars少了common/known而且名字确实与[=33中的"dict"不同=].
  • Example 4:建议 __class__。应该是type(obj)。我怀疑它与 __dict__ 的故事相似,尽管我认为 type 更为人所知。

关于隐私:在您自己的回答中,您说这些方法是 "semantically private"。我坚决不同意。单下划线和双下划线 leading 用于此,但数据模型的特殊 "dunder/magic" 方法具有双前导+尾部下划线。

  • 您用作参数的两件事是导入行为和 IDE 的自动完成。但是导入和这些特殊方法是不同的领域,我试过的IDE(流行的PyCharm)不同意你的看法。我用方法 _foo__bar__ 创建了一个 class/object 然后自动完成没有提供 _foo 提供 提供 __bar__.当我无论如何都使用这两种方法时,PyCharm 只警告我关于 _foo(称之为 "protected member"), 而不是 关于 __bar__ .
  • PEP 8 表示 'weak "internal use" indicator' 明确表示 前导下划线,明确表示双 leading 强调它提到了名称 mangling,后来解释说它用于 "attributes that you do not want subclasses to use"。但是关于 double leading+trailing 下划线的评论并没有这样说。
  • data model page you yourself link to says that these special method names"Python’s approach to operator overloading"。那里没有隐私。 private/privacy/protected 这个词甚至没有出现在那个页面的任何地方。

    我还建议阅读 this article by Andrew Montalenti 了解这些方法,强调 "The dunder convention is a namespace reserved for the core Python team""Never, ever, invent your own dunders" 因为 "The core Python team reserved a somewhat ugly namespace for themselves"。这都符合 PEP 8 的指令 "Never invent [dunder/magic] names; only use them as documented"。我认为 Andrew 是正确的——它只是核心团队的一个丑陋的名称空间。这是为了运算符重载,而不是隐私(不是安德鲁的观点,而是我和数据模型页面的观点)。

除了 Andrew 的文章外,我还检查了更多关于这些 "magic"/"dunder" 方法的文章,我发现其中 none 完全在谈论隐私。这不是本文的重点。

同样,我们应该使用 len(a),而不是 a.__len__()。但不是因为隐私。