魔术方法

魔术方法

可以将Python类定义为一个方法的长列表,在一定的情况下使用类的实例时,就可以调用这些方法。例如,类可以通过定义__eq__方法来定义该类的实例与另一个实例是否相等。如果类中存在__eq__方法的定义,那么类在使用“==”操作符进行相等性测试时,会自动调用该方法。
所谓的“魔术方法”被设计用于重载Python的操作符或内置方法。魔术方法可以通过“__”语法定义,从而避免程序员在没有意向使用重载时碰巧定义了同名方法。魔术方法使内置类(包括诸如整型和字符串等基本类型)提供的约定与自定义类提供的约定保持一致。如果希望在Python中进行相等性测试,可以总是使用“= =”完成该操作,而无须考虑比对对象是两个整数还是两个为某个特定应用程序写的类的实例,甚至是两个不相关类的实例。

(1)魔术方法语法

在Python中,魔术方法遵循统一的模式-----将下划线放到方法名称的两端。例如,当一个类的实例初始化时,将会执行__init__方法(而不是init)。
采用这种约定在一定程度上起到了未雨绸缪的作用。如果不以下划线作为方法名的开始与结束,可以按照自己的喜好命名方法,而无须考虑方法名之后被Python赋予特殊的(而非故意的)意义。
当口头提到该方法时,很多人选择使用杜撰的术语“dunder”描述魔术方法。因此,__init__最终的发音为dunder-init。
每个魔术方法都有特定的目的:当特定语法出现时,它作为执行的钩子(所谓钩子就是在特定事件发生时,能够为响应事件而调用的代码或函数,回调函数就算是钩子的一种类型)。

例如,__init__会在创建类的实例时执行:

>>> class MyClass:
		def __init__(self):
			print('The __init__method is running.')

		
>>> mc = MyClass()
The __init__method is running.

这里需要意识到的重点是并没有直接调用__init__方法,仅仅是Python的解释器知道在对象初始化时调用该方法。

所有的魔术方法都以这种方式生效,都需要特定的函数名称与方法签名(某些时候方法签名是一个变量),然后该方法就会在特定的情况下被调用。

前文提到的__eq__方法有两个强制参数:第一个是self参数和第二个作为比对对象的位置参数。

>>> class MyClass:
		def __eq__(self, other):
			return type(self) == type(other)


注意__eq__方法接收第二个参数,这是由于在Python中使用“==”进行相等性检查时,会执行__eq__方法,此时等号另一边的对象会赋值给该方法的第二个参数。
在本例中,__eq__仅仅是根据两个参数是否都为 MyClass 类的实例来检测是否相等,因此,将会得到如下结果:

>>> MyClass() == MyClass()
True
>>> MyClass() == 23
False
>>> mc = MyClass()
>>> mc2 = MyClass()
>>> mc == mc2
True

MyClass的两个不同实例相等,这是由于 isinstance(other, type(self)) 结果为True。

(2)可用的魔术方法

Python解释器能够识别大量用于不同目的的魔术方法,从对比检查和排序,到为实现多种语言特性的钩子。

(一)创建与销毁

该类方法在类的实例创建或销毁时执行。

1.__init__方法

在创建实例后会立即执行对象的__init__方法。该方法必须接收一个位置参数(self),然后可以接受任意数量的必要或可选位置参数,以及任意数量的关键字参数。
该方法的签名非常灵活,因为传递到类初始化调用的参数会被传递给__init__方法。

考虑下面包含__init__方法的类,该方法接受可选的关键字参数:

>>> import random
>>> class Dice:
		def __init__(self, sides = 6):
			self._sides = sides
		def roll(self):
			return random.randint(1, self._sides)

	

为了初始化一个标准的六面骰子,调用类时无需任何参数:d = Dice()。该代码创建了Dice实例,并调用了新实例的__init__方法,除了self参数外没有传递任何参数。由于并未提供 sides 参数,因此使用该参数的默认值6。
只需直接将 sides参数在调用 Dice 的构造函数时传递给它,该函数会把参数传递给__init__函数,而无须再创建一个 D20 类。

>>> d = Dice()
>>> d._sides
6
>>> d.roll()
3
>>> d.roll()
3
>>> d.roll()
4
>>> d.roll()
6
>>> d20._sides
20
>>> d20.roll()
10
>>> d20.roll()
20

值得注意的是,__init__方法并没有创建新对象(该操作由__new__完成)。该方法旨在为创建后的对象提供初始化数据。
实际上这意味着__init__方法并不返回(也不应该返回)任何值。在Python中所有的__init__都不返回值,如果用Return返回值则会导致TypeError错误。
__init__方法或许是在自定义类中用的最多的魔术方法。大多数类在初始化时都需要一些额外变量从而以某种方式自定义其实现,而__init__方法是实现这类逻辑最合适的方法。

2.__new__方法

__new__方法实际上是在__init__方法之前执行,用于创建类的实例。然而__init__方法负责在实例创建后对其进行自定义,__new__方法才是实际上创建并返回实例的方法。
__new__方法永远是静态的。同样它也无需显示装饰。第一个也是最重要的参数是创建实例所需要的类(按照惯例,命名为cls)。
在大多数情况下,__new__方法的其他参数会被完整复制到__init__方法中。参数在调用类构造函数时首先会被传递给__new__方法(这是由于其先被调用),然后在传递给__init__方法。
在实际应用中,大多数类无需定义__new__方法。该方法在Python中的内置实现已经足够。一个类需要定义该方法时,几乎都需要首先在实现本类逻辑之前引用父类的实现。
如下所式:

class MyClass(object):
	def __new__(cls, [...]):
		instance = super(MyClass, cls).__new__(cls, [...])
		[do work on instance]
		return instance

通常,会希望__new__方法返回一个已经被初始化后类的实例。但是某些情况下,并不需要这么做。注意,只有在通过__new__方法返回当前类的实例时才会执行__init__方法。如果返回的不是当前类的实例,那么就不调用__init__方法。
这样做主要是因为在某些情况下返回了其他类的实例,因此__init__也会被其他类中定义的__new__方法触发,而执行两个不同类的__init__方法很可能会导致问题。

3.__del__方法

__del__方法在对象被销毁时被调用。
在Python中,对于开发者来说很少会直接销毁对象(如果需要,应该使用 del 关键字销毁)。Python的内存管理机制能够很好地胜任这项工作,因此由垃圾回收器完成对象的销毁工作被普遍接受。
也就是说,不管对象以何种方式销毁都会触发__del__方法执行,无论是直接删除,或是由垃圾回收器进行内存回收。

>>> class Xon:
		def __del__(self):
			print('...delete object...')

如果创建了Xon对象但没有将其赋值给变量,那么它会被垃圾回收器标记为可回收,当其他语句执行时该对象会被垃圾回收器迅速回收。

>>> Xon()
<__main__.Xon object at 0x0000000002EC5CC0>
>>> a = 2
>>> a
...delete object...
2

在使用的特定版本的解释器中,内存操作导致垃圾回收器遍历表。找到Xon对象并删除它,这将触发该对象的__del__方法,该方法会在对象被传递到垃圾回收器时执行 del 方法内的输出语句并删除该对象。
如果直接删除Xon对象,也能看到类似的行为,如下:

>>> x = Xon()
>>> del x
...delete object...

在两种情况中,适用原则是一致的。无论是在代码中直接删除还是由垃圾回收器自动触发,都一样会调用__del__方法。
值得一提的是,__del__方法通常无法引发任何有意义的异常。这是由于该方法通常在后台由垃圾回收器触发,因此并没有一种好方法可以触发异常事件。因此在__del__方法中引发的任何异常都仅仅是在标准输出窗口打印一些错误信息,并且通常在该方法中引发异常并不合适。

(二)类型转换

在Python中存在多个用于将复杂对象转换为简单对象或常用数据类型的魔术方法。例如,str、int和bool等类型在Python中很常用,因此对于复杂对象来说知道自身使用简单类型的等价表示将会十分有用。

1.__str__方法

该方法接收一个位置参数 self,并在对象被传递给str的构造函数时被调用,然后返回一个字符串。

>>> class MyObject:
		def __str__(self):
			return 'My Awesome Object!'
	
>>> str(MyObject())
'My Awesome Object!'

字符串类型的使用十分普遍,因此为类定义一个__str__方法就变得很有用。
__str__方法还在其他特定场景被调用(本质上,底层调用__str__方法)。例如,当通过str的参数遇到格式化字符串%s时,如下所示:

>>> 'This is %s' % MyObject()
'This is My Awesome Object!'

注意:在Python2中,字符串使用ASCII字符串,而在Python3中,字符串使用Unicode字符串。在Python2中存在Unicode编码的字符串,而Python3引入了bytes(有时也叫bytestrings)类型,该类型基本可以类比Python2中的ASCII编码字符串。这些字符串家族拥有自己的魔术方法。Python2拥有__unicode__方法,该方法在对象传递给unicode的构造函数时被调用。类似的,Python3拥有__bytes__方法,该方法在对象传递给bytes的构造函数时被调用。上述两种情况中方法都会返回合适的类型。

2.__bool__方法

另一个常见需求是对象需要界定True或False,无论是通过表达式转换为布尔类型,还是在某些情况下需要该对象布尔类型的等价形式(比如在if语句中使用到该对象)。
该类型的操作在Python3中由__bool__魔术方法处理,而在Python2中该方法被命名为__nonzero__。无论是哪个版本,该方法都接受一个位置参数(self)并返回True或False。
通常无需显式定义一个__bool__方法。如果未定义__bool__方法,但定义了__len__方法,就将使用后者,从而会导致重复。

3.__int__方法、__float__与__complex__方法

某些情况下,将复杂对象转为基本类型的数字十分有价值。如果一个对象定义了一个返回int类型的__int__方法,那么该对象被传递给int的构造函数时,int方法会被调用。
类似地,如果对象定义了__float__与__complex__方法,那么这些方法会在各自传递给float或complex的构造函数时被调用。
注意:Python2拥有单独的Long类型,因此它具有__long__方法。该方法的原理与所期望的完全一致。

(三)比较

对象在进行相等性测试(==或!=)或不等性测试(例如,使用<、<=、>与>=)时进行比较。这些操作符与Python中的魔术方法一一对应。

1.二元相等性

下述方法支持使用==与!=进行相等性测试。

1)__eq__方法

__eq__方法在两个对象使用==操作符进行比较时被调用。该方法必须接受两个位置参数(按照惯例,分别为self和other),这是需要比较的两个对象。
在大多数情况下,首先检测左边对象的__eq__方法是否存在。如果左边对象定义了__eq__方法,那么该方法就会被使用(而且不再返回NotImplemented)。否则,则使用右边对象中定义的__eq__方法(参数位置对调)。
考虑下面的类,在比较相等性时打印了一些无意义的代码(并且返回False,除非是同一个对象),可以基于对象所在操作符的位置来查看顺序:

>>> class MyClass:
		def __eq__(self, other):
			print('The following are being tested for equivalence:\n%r\n%r' % (self, other))
			return self is other
	
>>> c1 = MyClass()
>>> c2 = MyClass()
>>> c1 == c2
The following are being tested for equivalence:
<__main__.MyClass object at 0x0000000002DCFF60>
<__main__.MyClass object at 0x0000000002DCF518>
False
>>> c2 == c1
The following are being tested for equivalence:
<__main__.MyClass object at 0x0000000002DCF518>
<__main__.MyClass object at 0x0000000002DCFF60>
False
>>> c1 == c1
The following are being tested for equivalence:
<__main__.MyClass object at 0x0000000002DCFF60>
<__main__.MyClass object at 0x0000000002DCFF60>
True

注意,对象输出到标准输出窗口的顺序被交换。这是由于这两个对象被传递到__eq__方法的顺序被交换而导致的。这还意味着在相等性检测时并没有对参数可交换的内在需求。除非有十分充足的理由,否则需要确保相等性检测始终可交换。
可以通过比较 MyClass对象与其他类型的对象来观察该行为的另一方面。考虑下面只有一个__eq__方法的类,该方法的作用仅仅是返回False:

>>> class Unequal:
	def __eq__(self, other):
		return False

当对这些类的实例进行相等性检测时,基于它们调用的顺序可以看到不同的特征。当MyClass的实例在左边时,会调用它的__eq__方法。当Unequal的实例在左边时,则会调用该类的方法。

>>> MyClass() == Unequal()
The following are being tested for equivalence:
<__main__.MyClass object at 0x0000000003051B00>
<__main__.Unequal object at 0x0000000003051BA8>
False
>>> Unequal() == MyClass()
False

对于传递给__eq__方法参数顺序的规则有一个例外:直接子类。如果被比较的两个对象中一个对象是另一个对象的直接子类,这会重载之前提到的规则,此时将使用子类的__eq__方法。

>>> class MySubclass(MyClass):
		def __eq__(self, other):
			print('MySubclass\' __eq__ method is testing:\n%r\n%r' % (self, other))
			return False

#现在,无论提供给操作符的参数的顺序如何,都会调用相同的方法并传入相同顺序的参数:
>>> MyClass() == MySubclass()
MySubclass' __eq__ method is testing:
<__main__.MySubclass object at 0x0000000003051DA0>
<__main__.MyClass object at 0x0000000003051C18>
False
>>> MySubclass() == MyClass()
MySubclass' __eq__ method is testing:
<__main__.MySubclass object at 0x0000000003051B00>
<__main__.MyClass object at 0x0000000003051C50>
False
2)__ne__方法

__ne__方法与__eq__方法的功能相反,但原理一致,仅仅是该方法在使用!=操作符时使用。
通常,无须调定义__ne__方法,只需要针对__eq__方法的返回值取反即可。如果没有定义__ne__方法,那么Python会调用__eq__方法,并对结果取反。
只有在不希望上述默认特征发生时才需要显式定义__ne__方法。

2.相对比较

以下这些方法也处理比较操作,但使用比较操作符来测试相对值(比如>)。

1)__lt__方法、__le__方法、__gt__方法和__ge__方法

__lt__方法、__le__方法、__gt__方法和__ge__方法分别与<、<=、>、>=操作符相匹配。与等值方法类似的是,上述方法都接受两个参数(按照惯例,为self和other),并在相对比较时根据运算符结果返回True或False。
通常,无须全部定义上面4个方法,Python解释器会认为__lt__是__ge__取反,gt__是__le__取反。同理,Python解释器会认为__le__是由__lt__与__eq__分离得来,ge__是由__gt__和__eq__分离得来。
这意味着,实际上通常只需要定义__eq__和__lt
(或__gt
),那么所有6个比较操作符就能按照预期正常生效。
另一个定义这些方法的重要(但很容易忽视的)方面是这些方法内置了用于排序对象的sorted函数。因此,如果有一个列表中的所有对象都定义了这些方法,将对象传递到sorted就会基于比较方法的结果自动返回一个升序排序后的列表。

3.操作符重载

这些方法提供了一种重载标准Python操作符的机制。

1)二元操作符

一系列魔术方法可用于重载Python中的多种二元操作符,如+和-等。实际上对于每一个操作符,Python都提供了3种魔术方法,每种方法都接收两个位置参数(按照惯例,为self和other)。
第一类方法是普通方法(vanilla method),表达式x + y 与 x.__add__(y)匹配,这类方法仅仅是返回结果。
第二类方法是取反方法(reverse method)。只有在第一个操作对象不提供传统方法并且操作对象类型不同(或返回NotImplemented)时才调用取反(操作符两边对象顺序交换)。这两类方法的拼写机制相同,只是取反方法在方法名称的开头加上了r。因此对于表达式x + y,如果x没有定义__add__方法,则调用y.__radd__(x)。
第三类也是最后一类方法,是即席方法(in-place method)。在操作符即席修改第一个变量时被调用(比如+=、-=等)。即席方法与第一类方法的拼写机制相同,只是这类方法仅仅是在正常方法开头加了i。因此表达式x+=y将会调用x.__iadd__(y)。
通常,即席方法仅仅即席修改self并返回它。但这并不是严格的需求。值得注意的是,仅在直接方法没有清晰匹配的情况下,才需要定义一个即席方法。如果未定义即席方法,那么就会调用直接方法将值赋给操作符左边的对象并返回。

操作符方法取反即席
+__add____radd____iadd__
-__sub____rsub____isub__
*__mul____rmul____imul__
/__truediv____rtruediv____itruediv__
//__floordiv____rfloordiv____ifloordiv__
%__mod____rmod____imod__
**__pow____rpow____ipow__
&__and____rand____iand__
|__or____ror____ior__
^__xor____rxor____ixor__
<<__lshift____rlshift____ilshift__
>>__rshift____rrshift____irshift__

这些方法可以重载Python中的所有二元操作符。在合理的情况下,自定义类可以(并且应该)重载二元操作符。

2)除法

最开始,在Python中的两个int型数据进行除法运算,返回值的类型为int,而不是float。基本原理是两个数进行除法运算然后向下取整。因此5/2将会返回2,-5/2将会返回-3,如果希望得到float数据类型的结果,则至少需要其中一个值的类型为float。因此5.0/2则返回2.5。
Python3改变了该特征。对两个int型数据做除法返回float类型的数据,即使结果是整数也是如此。因此5/2是2.5,4/2是2.0(不是2)。这是Python3语言引入的向后不兼容的变更。
由于Python3引入了向后不兼容的变更,因此Python2后续的版本使用了可以让开发人员“可选”新特征的机制:特殊模块__future__用于导入未来特征。在Python2.6和2.7中,开发人员可以通过加入from __future__ import divison 来“可选”Python3的特征。
__truediv__方法(与同类方法)是Python3的方法。Python2最初只提供__div__方法且对于/操作符调用该方法,除非从__future__中导入了/操作符,在这种情况下就是Python3中调用__truediv__的机制。
在大多数情况下,在Python2中执行的代码最终如何处理除法并不可知。这意味着需要同时定义__truediv__与__div__方法。在绝大多数情况下,这两个方法可以彼此匹配,这是完全可行的,如下所示:

>>> class MyClass:
	def __truediv__(self, other):
		[...]
	__div__ = __truediv__

	

或许使用__truediv__方法且将__div__作为别名才是“正确的”选择。宽泛的原则是最终在Python3下执行的代码应该以Python3为目标去编写,同时兼容Python2,而不是相反。

3)一元操作符

Python还提供了3个一元操作符:+、-与~。注意其中有两个操作符既是一元操作符也是二元操作符。这并不会引起问题,解释器能够根据表达式确定操作符被用于一元还是二元。
一元操作符方法只接收一个位置参数(self),执行操作并返回结果。这3个方法的名称分别为__pos__(与+匹配)、neg(与-匹配)和__invert__(与~匹配)。
一元操作符更加直接。比如表达式~x,就是调用x.__invert__()。

例:考虑下面的仿string类,该类可以以逆序的方式返回字符串
>>> class ReversibleString:
		def __init__(self, s):
			self.s = s
		def __invert__(self):
			return self.s[::-1]
		def __str__(self):
			return self.s

在Python解释器中,可以看到以下结果:

>>> rs = ReversibleString('I love you!')
>>> ~rs
'!uoy evol I'

注意返回值是一个str,而不是ReversibleString。这里并不需要相同类型的对象作为操作对象,__invert__方法也是如此。
通常情况下应该返回相同数据类型的对象。如下:

>>> class ReversibleString:
		def __init__(self, s):
			self.s = s
		def __str__(self):
			return self.s
		def __invert__(self):
			return type(self)(self.s[::-1])
		def __repr__(self):
			return 'ReversibleString: %s' % self.s

>>> rs = ReversibleString('xyz')
>>> ~rs
ReversibleString: zyx
>>> ~~rs
ReversibleString: xyz

注意:这里使用了type(self),而不是直接调用ReversibleString()。这确保了如果ReversibleString是子类,那么该子类可以正确使用。

4.重载常见方法

Python包含了很多内置方法(最常见的例子是len方法),该方法被广泛使用并将对象作为操作符。因此,Python提供了对象传递到这些方法时被调用的一些魔术方法。

1)__len__方法

该方法是以Python的方式确定一个条目的“长度”。
可以通过定义__len__方法描述对象的长度,该方法接收位置参数(self)并返回一个整型值。

>>> class Timesapn:
		def __init__(self, hours = 0, minutes = 0, seconds = 0):
			self.hours = hours
			self.minutes =minutes
			self.seconds = seconds
		def __len__(self):
			return (self.hours * 3600) + (self.minutes * 60) + self.seconds

	
>>> ts = Timesapn(hours = 2, minutes = 30, seconds = 1)
>>> len(ts)
9001

注意: 如果定义了__len__方法,通常还被用于确定对象被类型转换为bool或用于if语句时,其值为True还是False,除非该对象还同时定义了__bool__方法(或者在Python2中,定义了__nonzero__方法)。

>>> bool(Timesapn(hours = 1, minutes = 0, seconds = 0))
True
>>> bool(Timesapn(hours = 0, minutes = 0, seconds = 0))
False
2)__repr__方法

Python中一个最重要(也是经常被忽视的)方法是repr。任何对象都可以定义__repr__方法,该方法接收一个位置参数(self)。
为什么repr重要?对象的repr方法用于确定该对象在Python交互式终端中的显示方式。
在Python中,将对象生成为<__main__.object at Ox102cdf950>这种形式往往没有用处。在大多数情况下,一个对象的类以及其内存地址并不是希望获得的值。
定义__repr__方法可以让对象成为更有用的代表值。考虑下面定义了__repr__方法的Timespan类:

>>> class Timespan:
		def __init__(self, hours = 0, minutes = 0, seconds = 0):
			self.hours = hours
			self.minutes = minutes
			self.seconds = seconds
		def __repr__(self):
			return 'Timespan(hours = %d, minutes = %d, seconds = %d)' % (self.hours, self.minutes, self.seconds)

>>> Timespan()
Timespan(hours = 0, minutes = 0, seconds = 0)
>>> Timespan(minutes = 30, hours = 13)
Timespan(hours = 13, minutes = 30, seconds = 0)	

注意,除了显示所有Timespan的关键属性外,repr打印了一个用于初始化Timespan的有效表达式。在终端使用对象时,这非常有用。它能够直观地显示出正在使用的是一个对象,具体而言是Timespan对象。仅仅打印出时间信息使你无法确认所使用的对象是str或timedelta对象还是自定义的Timespan对象。
在此要 注意一个更重要的区别:repr和str的目的不同。如何描述其区别则取决于读取内容的不同。但简单来说,一个对象的repr供程序员阅读,而对象的str的用途更加广泛。

3)__hash__方法

hash函数的作用是通过数字化表达式唯一标识对象。
当一个对象传递给散列函数时,调用其__hash__方法(如果定义了hash)。__hash__方法接收一个位置参数(self),并返回一个整型值,该整型值可以为负数。
对象类提供了__hash__函数,通常返回该对象的id。对象的id值是与实现方式具体相关的,在CPython中,该值是其内存地址。
然而,如果一个对象定义了__eq__方法,则__hash__方法会隐式地被赋值为None。这样做是由于通常哈希值的目的很模糊。取决于对象的使用方式,每一个对象拥有哈希值且保持唯一是最佳实践,并且相等的对象其哈希值也应该相等。
因此,如果一个类能够理解相等且可哈希化时,其必须显式定义一个__hash__方法。
在Python中,哈希被用于多处。其中两个最常见的应用是用于字典的键值与set对象。仅有可哈希化的对象可以作为字典的键值。同样,仅有可哈希的对象可以在Python的set对象中存在。在上述两种情况下,哈希值用于确定一个对象是否是set对象的成员以及将某个对象与字典键值比对从而进行键查找。

4)__format__方法

该函数可以根据Python的格式化规范来格式化不同种类的对象。
任何对象都能提供__format__方法,该方法在对象被传递到format时调用。该方法接收两个位置参数,第一个为self,第二个为格式化规范的字符串。
在Python3中,str.format方法倾向使用__format__方法替换%操作符来处理字符串内的占位符。如果被传递给str.format的对象带有__format__方法作为参数,则将调用该方法。

>>> from datetime import datetime
>>> class MyDate(datetime):
		def __format__(self, spec_str):
			if not spec_str:
				spec_str = '%Y-%m-%d %H:%M:%S'
			return self.strftime(spec_str)

	
>>> md = MyDate(2012, 12, 21, 11)
>>> '{0}'.format(md)
'2012-12-21 11:00:00'
>>> '{0:%Y-%m-%d}'.format(md)
'2012-12-21'
5).__instancecheck__与__subclasscheck__方法

虽然在Python中大多数类型检查使用所谓的鸭子类型(duck typing)来完成,但还可以使用内置的isinstance方法检查一个对象是否是某个类的实例。类似的,可以使用issubclass检查一个类是否继承于另一个类。
极少需要自定义该特征。如果对象是所提供类或其任意子类的实例,isinstance方法返回True。同样,如果提供的两个参数是同一个类,issubclass(尽管名称表示的是子类)返回True。
尽管这样,偶尔也需要允许类伪装其身份。Python2.6引入了__instancecheck__与__subclasscheck__方法使其成为可能。这两种方法都接收两个参数,第一个是self,第二个参数是用于比较的类(与isinstance方法的第一个参数一样)。这允许类决定哪个对象可能会伪装成其实例或子类。

6).__abs__与__round__方法

Python提供了内置的abs与round函数,分别用于返回绝对值与取整后的值。
虽然很少需要自定义类的上述特征,但可以分别通过定义__abs__与__round__方法完成自定义。这两个方法都接收一个位置参数(self),并返回一个数字类型的值。

5.集合

很多对象都是其他不同种类对象的集合。最复杂的类功能上是来自属性的集合(以某种有意义的方式排序)与对象中定义的方法的集合。
Python通过几种方式来理解一个对象与另一个对象是否为“成员关系”。例如,对于列表和字典,可以通过表达式 needle in haystack(needle是被查找的对象,而haystack是一个集合)检查一个对象是否是一个集合的成员。
字典由键组成,并且可以通过 haystack[key] 基于键值进行查找。同样,大多数对象拥有属性,这些属性在初始化时设置或通过其他地方设置,可以使用点操作符访问(haystack.attr_name)。

Python提供了与所有这些对象交互的魔术方法。

1).__contains__方法

__contains__方法在对表达式(如 needle in haystack)求值时被调用。该方法接收两个位置参数(self和needle),如果needle在集合中,则返回True,否则返回False。
该方法并不能严格确保某一对象在一个集合中,虽然该方法经常用于这种情况下。
考虑下面这个表示一段时间区间的类:

>>> class DateRange:
		def __init__(self, start, end):
			self.start = start
			self.end = end
		def __contains__(self, needle):
			return self.start <= needle <= self.end

	
>>> from datetime import date
>>> dr = DateRange(date(2015, 1, 1), date(2015, 12, 31))
>>> date(2015, 4, 21) in dr
True
>>> date(2012, 4, 21) in dr
False
2).__getitem__方法、__setitem__和__delitem__方法

__getitem__方法及其同类方法被用于对集合(如字典)、索引或部分集合(如列表)进行键查找。在上述两种情况中,基本的表达式都是haystack[key]。
__getitem__方法接收两个参数:self与key。该方法在集合能找到元素时返回对应值,否则引发对应的异常。引发何种异常取决于具体情况,但通常都是IndexError、KeyError或TypeError。
__setitem__方法也用于同样情况下,但它用于设置集合中元素的值,而不是用于查找。该方法接收3个位置参数:self、key和value。并非所有支持元素查找的对象也同样需要支持元素修改。
__delitem__方法通常在使用del关键字(例如,del haystack[key])删除键时被调用。

3).__getattr__与__setattr__方法

Python类用作集合的另一种主要方式是作为属性与对象的集合。当date对象包含year、month和day时,这些都是属性。
无论是通过点(如obj.attr_name)还是使用getattr方法(比如 getattr(obj, ‘attr_name’)),__getattr__方法都会在试图获取一个对象的属性时被调用。
然而,与其他魔术方法不同,__getattr__仅在“用常规方式无法找到属性时才被调用”。
换句话说,Python解释器首先进行标准的属性查找,如果发现属性则返回。如果没有匹配属性(换句话说,引发AttributeError错误),只有在这种情况下才会调用__getattr__方法。
其工作机制类似之前讨论过的__getitem__()。该方法接收两个位置参数(self与key)并返回合适的值,或是引发AttributeError异常。
类似的,__setattr__的用法与__getattr__相同。其在赋值给一个对象时被调用,无论是通过点操作符还是使用setattr方法。与__getattr__不同的是,该方法一直会被调用(否则该方法没有存在的意义),因此,如果希望使用传统实现时,应该调用基类方法。

4).__getattribute__方法

__getattr__方法只有在无法找到属性时才被调用,因为这是在常规情况下希望的行为(否则会很容易陷入无限循环的大坑)。然而,__getattribute__与__getattr__不同,会被无条件调用。
在这里,逻辑顺序是首先调用__getattribute__方法,在正常情况下负责执行传统的属性查找。如果类定义了__getattribute__方法,其变为负责调用基类的实现。如果(并非只有在这种情况下)__getattribute__引发AttributeError异常,调用__getattr__方法。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值