前面两篇文章介绍了类与对象的基本概念和类中的一些成员,本篇主要介绍类和对象的特殊成员及一些高级特性。
(1)如果需要对一个对象(实例)进行格式化输出,可以重写类的__repr__()和__str__()方法。
两者的区别:使用交互式解释器输出对象时,结果是__repr__() 方法返回的字符串;使用 str() 或 print() 函数会输出__str__() 方法返回的字符串。参见下例:
class Point:
"""二维坐标系中的点"""
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
return "Point({0.x!r}, {0.y!r})".format(self)
def __str__(self):
return "({0.x!s}, {0.y!s})".format(self)
在交互式命令行【ipython】中的结果:
In [2]: point = Point(1, 2)
In [3]: point
Out[3]: Point(1, 2)
In [4]: print(point)
(1, 2)
可以看到,在交互式环境中格式化输出对象是Point(1, 2);而通过print()打印出的对象是(1, 2)。
(2) 注意,在格式化中使用 !r 表示输出使用 __repr__()来代替默认的__str__()。
In [5]: print("point is {!r}".format(point))
point is Point(1, 2)
In [6]: print("point is {!s}".format(point))
point is (1, 2)
In [7]: print("point is {}".format(point))
point is (1, 2)
如果 __str__() 没有被定义,会使用 __repr__() 来代替输出。通常来讲自定义 __repr__() 和 __str__() 是很好的习惯,因为它能简化调试和实例输出。
可以通过__doc__这个特殊字段获取类的描述【即类的注释】,用法 【类名.__doc__】,参见下列示例:
class A:
"""description..."""
def func(self):
pass
print(A.__doc__)
# description...
可以通过__dict__获取到类或对象的所有成员信息(字典形式),用法 【类名.__dict__】或者【对象.__dict__】,参见下例:
class A:
def __init__(self, name):
self.name = name
def func(self):
pass
# 获取类的所有成员
print(A.__dict__)
#{'__module__': '__main__', 'func': <function A.func at 0x00000000025976A8>, '__dict__': <attribute '__dict__' of 'A' objects>, '__weakref__': <attribute '__weakref__' of 'A' objects>, '__doc__': None}
# 获取对象的所有成员
obj = A("Liu You Yuan")
print(obj.__dict__)
# {'name': 'Liu You Yuan'}
可以看到类与对象成员之中,只有【普通字段】是存储在对象中的,其他成员都是在类中。参见笔者这篇文章:面向对象(二)【类的成员及修饰符】。
通过__class__能够获取当前操作的对象是由哪个类所创建,用法【对象.__class__】,参见下例:
class A:
def func(self):
pass
obj = A()
# 获取 [当前操作的对象] 所在的类名
print(obj.__class__)
# <class '__main__.A'>
通过__module__能够获取创建当前操作的对象的类所在的模块,用法【对象.__module__】,参见下例:
class A:
def func(self):
pass
obj = A()
# 获取 [当前操作的对象] 所在的模块名
print(obj.__module__)
# __main__
只需要在类中实现__iter__()方法,即可让对象作用于for循环。参见如下例子:
class A:
def __init__(self, lis):
self.lis = lis
def __iter__(self):
return iter(self.lis)
obj = A([2018, 0, 3, 1, 9])
for i in obj: # 当对象用于迭代时,实际是相当于迭代__iter__方法的返回值。
print(i)
# 2018
# 0
# 3
# 1
# 9
上例中,如果没有实现__iter__()方法,对象obj是不能被循环的。实际上,像list、dict、str等数据结构之所能够被迭代,就是其类中实现了__iter__()方法。
实现__getitem__ / __setitem__ / __delitem__, 可实现对象类似字典的操作,参见下例:
class Person:
def __init__(self, name):
self.name = name
def __getitem__(self, k):
return self.name
def __setitem__(self, k, v):
self.name = v
def __delitem__(self, k):
del self.name
obj = Person("Jeo Chen")
result = obj['name'] # 自动触发执行 __getitem__
print(result) # Jeo Chen
obj['name'] = 'Liu You Yuan' # 自动触发执行 __setitem__
print(obj['name']) # Liu You Yuan
del obj['name'] # 自动触发执行 __delitem__
如果需要创建大量(成千上万)的对象,导致很占内存,可以通过特殊的静态字段__solts__来减少对象所占用的内存。
下例将对比定义 __solts__ 和 没有定义 __solts__ 的两个类在创建大量对象时占用的内存大小,其中用了【反射的知识】和 【tracemalloc包】。
tracemalloc包是跟踪由Python分配的内存块的调试工具。其中:
(1)tracemalloc.start()方法表示开始跟踪Python内存分配,开始时内存占用设为1;tracemalloc.stop()表示停止跟踪;
(2)tracemalloc.get_traced_memory()方法能获取由 tracemalloc 模块跟踪的内存块的当前大小和峰值大小作为元组:(current: int, peak: int),单位为字节。
详细参见下例:
import tracemalloc
ITEM_NUM = 10
class HaveSlots:
__slots__ = ['item%s' % i for i in range(ITEM_NUM)]
def __init__(self):
for i in range(len(self.__slots__)):
setattr(self, 'item%s' % i, i)
class NoSlots:
def __init__(self):
for i in range(ITEM_NUM):
setattr(self, 'item%s' % i, i)
# 开始跟踪
tracemalloc.start()
obj = [NoSlots() for i in range(100)]
# 获取由 tracemalloc 模块跟踪的内存块的当前大小和峰值大小作为元组:(current: int, peak: int)
print(tracemalloc.get_traced_memory())
# 停止跟踪
tracemalloc.stop()
# 又开始跟踪,相当于重置
tracemalloc.start()
obj2 = [HaveSlots() for i in range(100)]
print(tracemalloc.get_traced_memory())
# (21832, 22219) # 未定义__slots__字段,创建100个对象占用的内存约为 21832 字节
# (13760, 14147) # 定义__slots__字段,创建100个对象占用的内存约为 13760 字节
上例可见,当定义了__slots__字段时, 创建大量对象所占用的内存(13760) 明显小于 没有定义__slots__字段的内存(21832)。或许,你得到的占用内存大小与我得到的不一致,但不影响最终结论。
__slots__究竟做了什么来降低内存呢?
(1)默认情况下,自定义的对象都使用dict来存储属性(通过obj.__dict__查看),而python中的dict的底层需要考虑“降低hash冲突”,因此dict所占存储空间要比实际存储的元素大,会浪费一定的空间。
(2)使用__slots__后的类所创建的对象只会用到这些_slots__定义的字段,也就是说,每个对象都是通过一个很小的固定大小的数组来构建字段,而不是字典。
需要注意的是:
(1)如果声明了__slots__,那么对象就不会再有__dict__属性。
(2)使用__slots__意味着不能再给实例添加新的属性,只能使用在 __slots__ 中定义的那些属性名。
(3)定义了__slots__后的类不再支持一些普通类特性了,比如多继承。
因此,如果需要创建成千上万的对象,__slots__比较适用;其他情况,还是要减少对__slots__使用的冲动。
类中的__init__()方法就是类的构造方法了,通过类创建对象时,自动触发执行。参见下例:
class A:
def __init__(self, name):
self.name = name
print("this is __init__")
# 创建对象则自动触发__ini__方法。
obj = A("Jeo Chen")
# this is __init__
直到此时,才介绍构造方法其实是为后面的内容铺垫。这里说创建对象时自动触发执行构造方法是不准确的,继续往下读,会介绍__init__的真正作用。
__del__方法即为类的析构方法,当对象在内存中被释放时,自动触发执行。不过,Python是有垃圾回收机制的高级语言,我们无需关心内存的分配和释放。解释器在进行垃圾回收时自动触发执行的析构方法。
class A:
def __del__(self):
pass
当在对象后面加括号,即 【对象()】会自动触发__call__方法,这一点要与构造方法相区别。构造方法是类名后面加括号,即【类名()】触发。
class A:
def __call__(self):
print("this __call__")
obj = A()
obj() # 对象后面加括号,触发__call__
# this __call__
实际上,在创建对象时,调用__init__方法之前,就调用了__new__方法。我们可以通过下例证明:
class A:
def __init__(self, name):
print("In A init")
self.name = name
def __new__(cls, *args, **kwargs):
print("In A new",)
return object.__new__(cls)
# 创对象
obj = A("Liu You Yuan")
# In A new
# In A init
那么__new__方法到底有什么作用?下例演示了不调用__init__方法创建一个对象,详见如下:
class Person:
def __init__(self, name):
print("in Person init")
self.name = name
def __new__(cls, *args, **kwargs):
print("In Person new",)
return object.__new__(cls)
# 不调用 __init__() 方法来创建Person对象
obj = Person.__new__(Person)
print(obj)
print(obj.name)
执行结果如下:
In Person new
<__main__.Person object at 0x00000000025E8EB8>
Traceback (most recent call last):
File "D:/githubfile/pythonclub/面向对象/new.py", line 34, in <module>
print(obj.name)
AttributeError: 'Person' object has no attribute 'name'
分析输出结果:
(1)第一行打印了 "In Person new" 说明确实是调用了__new__方法;另外并没有打印 "in Person init" 说明确实没有调用__init__方法。
(2)第二行打印了"<__main__.Person object at 0x00000000025E8EB8>", 说明确实创建了一个对象。
(3)接着有报错,报错内容说"AttributeError: 'Person' object has no attribute 'name'", 对象并没有name属性(字段)。
综上,上例的结果不言而喻:
(1)__new__方法才是真正创建对象的,只不过它创建的对象在没调用__init__前是没有经过【初始化】的。
(2)__init__方法是初始化对象的,【初始化】的过程也就是将字段封装到对象中,通过 对象.字段 就能访问。
常常听到,"python 一切皆对象", 如此,"类" 本身也是对象,既然是对象,必然有创建它的类。换言之,"类"这个对象,是由"某个特殊的类"实例化而来。这个特殊的类就是type(),又称元类。
除了之前介绍的通过class关键字可以定义类,通过type()也能定义,参见下例:
def __init__(self, name):
self.name = name
def hello(self):
print("hello {}".format(self.name))
# 用type()定义类。第一个参数是类名,第二个参数是当前类的基类,第三个参数为类的成员
Person = type('Person', (object,), {'sayHi': hello, "__init__": __init__})
obj = Person("Liu")
print(obj) # <__main__.Person object at 0x0000000002368E80>
obj.sayHi() # hello Liu
这样证实了通过元类type()也能定义一个类,而且跟用class关键字定义的效果一样,只不过不常用这种方式罢了。
type()和我们平常创建类和对象有什么关系呢?我们可以通过下例一探究竟:
class MyType(type):
def __init__(self, child_cls, bases=None, dict=None):
print("In MyType init")
super(MyType, self).__init__(child_cls, bases, dict)
def __new__(cls, *args, **kwargs):
print("In MyTyPe new")
return type.__new__(cls, *args, **kwargs)
def __call__(self, *args, **kwargs):
print("In MyType call")
obj = self.__new__(self, args, kwargs)
self.__init__(obj, *args, **kwargs)
class Person(object, metaclass=MyType):
def __init__(self, name):
print("In Person init")
self.name = name
def __new__(cls, *args, **kwargs):
print("In Person new",)
return object.__new__(cls)
在上例代码中,值得注意的是:
(1)MyType继承了type类,同时自定义了__init__ / __new__ / __call__方法。
(2)Person类中有个参数metaclass,用来指定创建Person的类, 也就是metaclass指定了由MyType这个类通过实例化,创建Person这个对象。
(3)这里多说一句,于我们而言,Person是我们定义的一个类;于MyType而言,Person是MyType创建的一个对象。
上例输出结果如下:
In MyTyPe new
In MyType init
这可以看出我们只是定义了两个类,做了一些自定义修改。当运行上述代码时,就已经调用了MyType类的__new__和__init__方法了,也就是这时候通过MyType已经创建好了Person这个对象了。
接着我们在上例中添加一行,再运行:
obj = Person("Liu You Yuan")
运行结果如下:
In MyTyPe new
In MyType init
In MyType call
In Person new
In Person init
分析:
(1)当执行代码obj = Person("Liu You Yuan"), 我们把Person看成是MyType创建的对象,那么此行代码就是在Person对象后面加了括号,这就会触发MyType类中的__call__方法,打印“In MyType call”;
(2)此时,__call__中的self就是Person这个对象,通过self.__new__ / self.__init__主动调用了Person中的__new__ 和__init__,这也就创建出了obj这个对象。
至此,我们知道,当我们创建一个类A,并通过类A创建对象obj时,实际上是经历了两个过程:
(1)通过元类type创建我们定义的类A。
(2)通过类A创建对象obj。
本篇完。