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

时间:2016-10-26 21:09:32

标签: python python-datamodel

当我在Stack Overflow上阅读Python答案时,我会继续直接看到某些人telling usersuse the data model's特殊methodsattributes

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

为什么?特殊的“dunder”方法与Python data modelbuiltin functions的属性之间有什么关系?

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

2 个答案:

答案 0 :(得分:38)

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

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

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

语义内部API比公共接口更有可能发生变化。虽然Python实际上并没有考虑任何事情而且#34;私有"暴露内部,这并不意味着滥用访问权限是一个好主意。这样做有以下风险:

  • 升级Python可执行文件或切换到Python的其他实现(如PyPy,IronPython或Jython,或其他一些无法预料的实现)时,您可能会发现有更多重大更改。
  • 你的同事可能会认为你的语言能力和责任感很差,并认为这是一种代码嗅觉,使你和你的其余代码受到更严格的审查。
  • 内置函数很容易拦截行为。使用特殊方法直接限制了Python的内省和调试功能。

深度

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

内置函数和运算符也可以具有后备或更优雅的行为,而不是更原始的数据模型特殊方法。例如:

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

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

将内置运算符和运算符映射到datamodel

这是内置函数和运算符的映射,它们使用或返回各自的特殊方法和属性 - 请注意,通常的规则是内置函数通常映射到同名的特殊方法,但这不足以保证在下面给出这张地图:

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__() 

虚线查找

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

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__

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'代表属性名称:

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)

如果dict找不到密钥,则会调用__missing____getitem__的子类的特例:

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)

和一元行动:

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

上下文管理器

上下文管理器定义__enter__,在输入代码块时调用它(其返回值,通常为self,与as别名),__exit__,保证为在离开代码块时被调用,但有异常信息。

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

如果__exit__获得异常然后返回false值,它将在离开方法时重新加载它。

如果没有异常,__exit__会为这三个参数获取None,并且返回值毫无意义:

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

一些元类特殊方法

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

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

重要的一点是,尽管像nextbool这样的内置函数在Python 2和3之间没有变化,但基础实现名称​​正在更改。

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

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

在Python中,以下划线开头的名称是用户的语义非公共名称。下划线是创作者的说法,&#34;放手,不接触。&#34;

这不仅仅是文化,而且也是Python对API的处理。当程序包__init__.py使用import *从子包提供API时,如果子包未提供__all__,则会排除以下划线开头的名称。子包装__name__也将被排除在外。

IDE自动完成工具考虑到以下划线开头的非公开名称。不过,我非常感谢您没有看到__init____new____repr____str____eq__等(也没有看到任何用户创建的非公共接口)当我输入一个对象的名称和一个句点时。

因此我断言:

特别&#34; dunder&#34;方法不是公共接口的一部分。避免直接使用它们。

那么什么时候使用它们?

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

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

在函数或类

上使用__name__特殊属性

当我们装饰一个函数时,我们通常得到一个包装函数作为回报,隐藏有关函数的有用信息。我们会使用@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

类似地,当我需要方法中的对象类的名称(例如,用于__repr__)时,我会执行以下操作:

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

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

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

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

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) 

结论

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

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

答案 1 :(得分:11)

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

我同意您自己的回答,例如应使用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。不,我已经拥有了一个可以使用的非常好的功能:set的__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)并使用推理“如果您的键盘缺少方括号键”,这很少适用,而且我怀疑是严重的。我认为这只是最后一点的关键所在,提到我们可以在自己的类中支持方括号语法。这使得它回到建议使用方括号。
  • Example 3:建议obj.__dict__。不好,就像__len__示例一样。但我怀疑他只是不知道vars(obj),我可以理解它,因为vars不太常见/已知且名称与__dict__中的“字典”不同。 / LI>
  • Example 4:建议__class__。应该是type(obj)。我怀疑它与__dict__故事类似,但我认为type更为人所知。

关于隐私:在您自己的回答中,您说这些方法是“语义上私密的”。我非常不同意。单个和双重前导下划线就是这样,但不是数据模型的特殊“dunder / magic”方法,带有双前导+尾部下划线。

  • 您用作参数的两件事是导入行为和IDE的自动完成。但是导入和这些特殊方法是不同的区域,我尝试的一个IDE(流行的PyCharm)不同意你的看法。我使用方法_foo__bar__创建了一个类/对象,然后自动完成没有提供_foo提供提供__bar__。无论如何,当我使用这两种方法时,PyCharm只警告我_foo(称之为“受保护成员”),关于__bar__
  • PEP 8明确表示弱“内部使用”指标 单个前导下划线,并明确表示双前导下划线它提到名称mangling,后来解释说它是“属性,你不希望子类使用”。但关于双重领先+尾随下划线的评论并没有说明这一点。
  • 您自己链接的data model page表示这些special method names“Python的操作符重载方法”。那里没有关于隐私的事情。 private / privacy / protected这个词甚至不会出现在该页面的任何地方。

    我还建议阅读关于这些方法的this article by Andrew Montalenti,强调“dunder约定是为核心Python团队保留的命名空间”“永远不会发明你自己的dunders” 因为“核心Python团队为自己保留了一个有点丑陋的命名空间”。所有这些都符合PEP 8的指令“永远不要发明[dunder / magic]名称;只能使用它们作为记录”。我认为安德鲁是现实 - 它只是核心团队的一个丑陋的命名空间。这是出于操作员重载的目的,而不是隐私(不是安德鲁的观点,而是我的和数据模型页面)。

除了Andrew的文章,我还检查了几个关于这些“魔术”/“dunder”方法的内容,我发现它们都没有谈论隐私。这不是什么意思。

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