【问题标题】:What is the relationship between the Python data model and built-in functions?Python数据模型和内置函数有什么关系?
【发布时间】:2017-03-09 09:40:29
【问题描述】:

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

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

这是为什么呢? Python data model 和 builtin functions 的特殊“dunder”方法和属性之间有什么关系?

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

【问题讨论】:

    标签: python python-datamodel


    【解决方案1】:

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

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

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

    语义上的内部 API 比公共接口更有可能发生变化。虽然 Python 实际上并没有考虑任何“私有”并公开内部结构,但这并不意味着滥用该访问权限是一个好主意。这样做有以下风险:

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

    深入

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

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

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

    (如果需要或需要,也可以在模块的全局范围或builtins 模块上轻松地覆盖内置函数,以进一步自定义行为。)

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

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

    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(), **, &lt;&lt;, &gt;&gt;, &amp;, ^, |运算符也有特殊的方法,例如:

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

    以及用于扩充赋值的就地运算符,+=, -=, *=, @=, /=, //=, %=, **=, &lt;&lt;=, &gt;&gt;=, &amp;=, ^=, |=,例如:

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

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

    和一元运算:

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

    上下文管理器

    上下文管理器定义__enter__,在进入代码块时被调用(它的返回值,通常是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__ 得到一个异常然后返回一个假值,它会在离开方法时重新引发它。

    如果没有例外,__exit__ 会为这三个参数获取None,返回值是没有意义的:

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

    一些元类特殊方法

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

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

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

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

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

    在 Python 中,以下划线开头的名称在语义上是用户的非公共名称。下划线是创作者的表达方式,“放手,不要碰。”

    这不仅仅是文化问题,Python 对 API 的处理方式也是如此。当包的__init__.py 使用import * 提供来自子包的API 时,如果子包不提供__all__,则排除以下划线开头的名称。子包的__name__ 也将被排除在外。

    IDE 自动完成工具在将下划线开头的名称视为非公开名称时混为一谈。但是,我非常感谢在我键入对象名称时没有看到__init__、__new__、__repr__、__str__、__eq__ 等(也没有任何用户创建的非公共接口)一个句号。

    因此我断言:

    特殊的“dunder”方法不是公共接口的一部分。避免直接使用它们。

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

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

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

    在函数或类上使用__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 的情况下使用特殊方法。

    【讨论】:

    • 我认为里程会有所不同,具体取决于您使用的 dunder 属性/方法。例如self.__dict___ 可能几乎和vars(self) 一样普遍,self.__class__ 也几乎和type(self) 一样受欢迎。作为一个简单的概括,我认为不使用查找属性的方法更有可能被社区认为是“可接受的”,而不是调用钩子的方法。话虽如此,我与您的结论完全一致:“尽可能使用内置函数/运算符”
    • 注意到在类上调用特殊方法而不是在实例上可能会很有趣。例如,next(obj) 并不能完全转换为 obj.__next__(),而是类似于 type(obj).__next__(obj)。
    【解决方案2】:

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


    我同意您自己的回答,例如应该使用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 s 或def 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__ 中的“dict”不同。
    • 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 的运算符重载方法”。那里没有关于隐私的事情。私人/隐私/受保护这些词甚至不会出现在该页面的任何位置。

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

    除了 Andrew 的文章之外,我还查看了更多关于这些“魔术”/“dunder”方法的内容,但我发现其中没有一个是在谈论隐私。这不是本文的目的。

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

    【讨论】:

    • 我认为type(obj)与obj.__class__和len(a)与a.__len__()之间的另一个(关键)区别是内置type过载。 type(obj) 的意思与 type('Foo', (object,), {}) 完全不同。 vars() 与 vars(obj) 类似。与其记住type 做了很多事情,我认为人们倾向于将type 视为创建新类型的工厂,因此他们回退到好的ole'obj.__class__ 来获取对象的类型。我并不是说他们应该,只是想解释为什么它可能比__len__ misuse 更常见
    • @mgilson 好点。或者他们甚至只知道另一件事。就像,他们在某处看到 type 被用于创建类型,然后从未学习过其他东西,因为他们认为他们已经知道 type 做了什么。另一件事可能是 IDE 的自动完成功能。人们可以通过查看自动完成功能来了解他们可以对对象做什么。然后他们在那里看到__class__,但看不到type。
    猜你喜欢
    • 1970-01-01
    • 2012-11-30
    • 2015-01-31
    • 2011-11-04
    • 1970-01-01
    • 2019-05-04
    • 2017-11-19
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多