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(), **, <<, >>, &, ^, |运算符也有特殊的方法,例如:
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 += 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 的情况下使用特殊方法。