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 上。
(如果必要或需要,内置函数也可以在模块的全局范围或
这里有一个内置函数和运算符到它们使用或返回的各自特殊方法和属性的映射(带有注释)-注意,通常规则是内置函数通常映射到相同名称的特殊方法,但这不足以保证在下面给出此映射:好的。
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__() |
1 | length_hint(obj) obj.__length_hint__() |
点状查找
点式查找是上下文相关的。在没有特殊方法实现的情况下,首先在类层次结构中查找数据描述符(如属性和槽),然后在实例
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 |
描述符
描述符有点高级——可以跳过这些条目稍后返回——回想一下,描述符实例在类层次结构中(如方法、槽和属性)。数据描述符实现
1 2 3 | obj.attr descriptor.__get__(obj, type(obj)) obj.attr = val descriptor.__set__(obj, val) del obj.attr descriptor.__delete__(obj) |
当类被实例化(定义)时,如果有任何描述符要通知描述符其属性名,则调用以下描述符方法
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) |
如果
1 | obj[name] -> obj.__missing__(name) |
算子
对于
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__() |
上下文管理器
上下文管理器定义在输入代码块时调用的
1 2 3 | with obj as cm: -> cm = obj.__enter__() raise Exception('message') -> obj.__exit__(Exception, Exception('message'), traceback_object) |
如果
如果没有例外,那么
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) |
重要的一点是,虽然像
因此,使用内建还提供了更多的前向兼容性。好的。我什么时候可以用这个特殊的名字?
在Python中,以下划线开头的名称在语义上是用户的非公共名称。下划线是造物主的表达方式,"放开手,不要触摸。"好的。
这不仅是文化因素,而且在python对api的处理中也是如此。当包的
IDE自动完成工具在考虑以下划线开头的非公共名称时混合使用。但是,我非常感谢在我输入一个对象和一个期间的名称时,没有看到
因此我断言:好的。
特殊的"dunder"方法不是公共接口的一部分。避免直接使用。好的。
那么什么时候使用它们呢?好的。
主要的用例是在实现自己的自定义对象或内置对象的子类时。好的。
尽量只在绝对必要的时候使用它们。以下是一些例子:好的。在函数或类上使用
当我们修饰一个函数时,我们通常会得到一个包装函数作为返回,它隐藏了关于该函数的有用信息。我们将使用
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 |
同样,当我需要方法中对象类的名称时(例如,在
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 |
然而,即使在这种情况下,我们也不使用
另一个需要使用特殊方法名的点是,当我们在子方法的实现中,并且希望委托给父方法名。例如:好的。
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文档的情况下使用特殊方法。好的。好啊。
我将展示一些您显然没有想到的用法,对您展示的示例进行评论,并反驳来自您自己答案的隐私声明。好的。
我同意你自己的回答,例如应该使用
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 |
但除了在我自己的类中定义这些方法供内置函数和运算符使用之外,我偶尔也会使用它们,如下所示:好的。
假设我需要给某个函数一个过滤函数,我想用一个集合
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 |
所以,虽然我没有直接调用像
我对你展示的例子的看法是:好的。
- 例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的指示"永远不要编造[邓德/魔法]的名字,只使用记录在案的名字"。我认为安德鲁是当务之急——这只是核心团队的一个丑陋的名称空间。它的目的是为了操作人员过载,而不是为了隐私(不是安德鲁的观点,而是我的和数据模型页的观点)。
除了安德鲁的文章之外,我还查阅了一些关于这些"魔法"/"邓德"方法的文章,我发现他们中没有一个人在谈论隐私。这不是问题所在。好的。
同样,我们应该使用