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

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

当我阅读关于堆栈溢出的python答案时,我继续看到一些人告诉用户直接使用数据模型的特殊方法或属性。

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

为什么会这样?python数据模型和内置函数的特殊"dunder"方法和属性之间有什么关系?

我什么时候可以用这个特殊的名字?


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

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

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

语义上的内部API比公共接口更容易更改。虽然python实际上不考虑任何"私有"的内容,并且暴露了内部结构,但这并不意味着滥用该访问是个好主意。这样做有以下风险:好的。

  • 当升级python可执行文件或切换到python的其他实现(如pypy、ironpython或jython,或其他一些不可预见的实现)时,您可能会发现有更多破坏性的更改。
  • 你的同事可能会对你的语言技能和责任心不太重视,认为这是一种代码味道,会让你和其他代码受到更严格的审查。
  • 内置函数很容易拦截的行为。使用特殊的方法直接限制了Python的自省和调试能力。

深入地

内置函数和运算符调用特殊方法并使用Python数据模型中的特殊属性。它们是隐藏物体内部的可读和可维护的单板。通常,用户应该使用语言中给定的内置函数和运算符,而不是直接调用特殊方法或使用特殊属性。好的。

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

  • 当迭代器用完时,next(obj, default)允许您提供一个缺省值而不是提升StopIteration,而obj.__next__()不允许。
  • obj.__str__()不可用时,str(obj)退回到obj.__repr__(),而直接调用obj.__str__()会引起属性错误。
  • 在python 3中,当没有__ne__调用obj.__ne__(other)时,obj != other会退到not obj == other上。

(如果必要或需要,内置函数也可以在模块的全局范围或builtins模块上很容易被遮盖,以进一步自定义行为。)好的。将内置和运算符映射到数据模型

这里有一个内置函数和运算符到它们使用或返回的各自特殊方法和属性的映射(带有注释)-注意,通常规则是内置函数通常映射到相同名称的特殊方法,但这不足以保证在下面给出此映射:好的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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__模块,则该模块通过各自的特殊方法实现回退:好的。

1
length_hint(obj)  obj.__length_hint__()

点状查找

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

1
2
3
4
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

描述符

描述符有点高级——可以跳过这些条目稍后返回——回想一下,描述符实例在类层次结构中(如方法、槽和属性)。数据描述符实现__set____delete__:好的。

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

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

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

项目(下标符号)

下标符号也与上下文相关:好的。

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

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

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

算子

对于+, -, *, @, /, //, %, divmod(), pow(), **, <<, >>, &, ^, |操作符,还有一些特殊的方法,例如:好的。

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

以及用于增强分配的就地运算符,+=, -=, *=, @=, /=, //=, %=, **=, <<=, >>=, &=, ^=, |=,例如:好的。

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

一元运算:好的。

1
2
3
+obj          ->  obj.__pos__()
-obj          ->  obj.__neg__()
~obj          ->  obj.__invert__()

上下文管理器

上下文管理器定义在输入代码块时调用的__enter__(其返回值通常为self,别名为as)和__exit__,保证在离开代码块时调用,但不包括信息。好的。

1
2
3
with obj as cm:     ->  cm = obj.__enter__()
    raise Exception('message')
->  obj.__exit__(Exception, Exception('message'), traceback_object)

如果__exit__得到一个异常,然后返回一个错误的值,那么它将在离开方法时重新发出该值。好的。

如果没有例外,那么__exit__会得到这三个参数的None,返回值没有意义:好的。

1
2
3
with obj:           ->  obj.__enter__()
    pass
->  obj.__exit__(None, None, None)

一些元类特殊方法

类似地,类可以具有支持抽象基类的特殊方法(从其元类中):好的。

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

重要的一点是,虽然像nextbool这样的内置组件在python2和3之间没有变化,但底层实现名称正在变化。好的。

因此,使用内建还提供了更多的前向兼容性。好的。我什么时候可以用这个特殊的名字?

在Python中,以下划线开头的名称在语义上是用户的非公共名称。下划线是造物主的表达方式,"放开手,不要触摸。"好的。

这不仅是文化因素,而且在python对api的处理中也是如此。当包的__init__.py使用import *从子包提供api时,如果子包不提供__all__则不包括以下划线开头的名称。子包的__name__也将被排除在外。好的。

IDE自动完成工具在考虑以下划线开头的非公共名称时混合使用。但是,我非常感谢在我输入一个对象和一个期间的名称时,没有看到__init____new____repr____str____eq__等(也没有看到任何用户创建的非公共接口)。好的。

因此我断言:好的。

特殊的"dunder"方法不是公共接口的一部分。避免直接使用。好的。

那么什么时候使用它们呢?好的。

主要的用例是在实现自己的自定义对象或内置对象的子类时。好的。

尽量只在绝对必要的时候使用它们。以下是一些例子:好的。在函数或类上使用__name__特殊属性

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

1
2
3
4
5
6
7
8
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

同样,当我需要方法中对象类的名称时(例如,在__repr__中使用),我会执行以下操作:好的。

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

使用特殊属性编写自定义类或子类内置项

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

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

1
2
3
4
5
6
7
8
9
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)(见我这里的答案,以证明后者可能导致意外行为),相反,我们应该使用更高级别的抽象。好的。

另一个需要使用特殊方法名的点是,当我们在子方法的实现中,并且希望委托给父方法名。例如:好的。

1
2
3
4
5
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)

结论

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

尽可能使用内置函数和运算符。仅在没有公开API文档的情况下使用特殊方法。好的。好啊。


我将展示一些您显然没有想到的用法,对您展示的示例进行评论,并反驳来自您自己答案的隐私声明。好的。

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

1
2
3
4
5
6
7
8
9
>>> 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。不,我已经有了一个可以使用的非常好的函数:set的__contains__方法。它更简单,更直接。而且速度更快,如图所示(忽略我在这里将其保存为f,这只是用于这个计时演示):好的。

1
2
3
4
5
6
7
>>> 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}
def f(x): return x in s'
, number=10**8)
20.445680107760325

所以,虽然我没有直接调用像s.__contains__(x)这样的魔法方法,但我偶尔会将它们传递到像some_function_needing_a_filter(s.__contains__)这样的地方。我认为这非常好,而且比lambda/def替代品更好。好的。

我对你展示的例子的看法是:好的。

  • 例1:当被问到如何获得清单的大小时,他回答了items.__len__()。即使没有任何理由。我的结论是:那是错误的。应该是len(items)
  • 例2:首先提到d[key] = value!然后在d.__setitem__(key, value)中加上"如果你的键盘缺少方括号键",这很少见,我怀疑这很严重。我认为这只是最后一点的切入点,提到这就是我们如何在自己的类中支持方括号语法。这又回到了使用方括号的建议。
  • 例3:建议使用obj.__dict__。不好,比如__len__例子。但我怀疑他只是不认识vars(obj),我可以理解,因为vars不太常见/不太知名,而且名字与__dict__中的"dict"不同。
  • 例4:建议使用__class__。应该是type(obj)。我怀疑这和__dict__的故事很相似,尽管我认为type更有名。

关于隐私:在你自己的回答中,你说这些方法是"语义上私有的"。我强烈反对。单前导和双前导下划线用于此目的,但数据模型的特殊"dunder/magic"方法不支持双前导+尾随下划线。好的。

  • 作为参数使用的两件事是导入行为和IDE的自动完成功能。但是导入和这些特殊方法是不同的领域,我尝试的一个IDE(流行的Pycharm)与您不一致。我用_foo__bar__方法创建了一个类/对象,然后自动完成没有提供_foo,但提供了__bar__。不管怎样,当我使用这两种方法时,Pycharm只是警告我关于_foo(称之为"受保护成员"),而不是关于__bar__
  • PEP8明确表示单前导下划线的"弱"内部使用"指示符",并明确表示双前导下划线的"弱"内部使用"指示符",它提到了名称混乱,后来解释说它用于"不希望子类使用的属性"。但是关于双前导+尾随下划线的评论并没有这样说。
  • 您自己链接到的数据模型页面说,这些特殊的方法名是"Python的运算符重载方法"。这里没有隐私。private/privacy/protected这个词甚至不会出现在那个页面的任何地方。我还建议阅读AndrewMontanti关于这些方法的文章,强调"Dunder约定是为核心python团队保留的名称空间","永远不要发明自己的Dunder",因为"核心python团队为自己保留了一个有点难看的名称空间"。所有这些都符合Pep8的指示"永远不要编造[邓德/魔法]的名字,只使用记录在案的名字"。我认为安德鲁是当务之急——这只是核心团队的一个丑陋的名称空间。它的目的是为了操作人员过载,而不是为了隐私(不是安德鲁的观点,而是我的和数据模型页的观点)。

除了安德鲁的文章之外,我还查阅了一些关于这些"魔法"/"邓德"方法的文章,我发现他们中没有一个人在谈论隐私。这不是问题所在。好的。

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