前面两篇文章介绍了类与对象的基本概念和类中的一些成员,本篇主要介绍类和对象的特殊成员及一些高级特性。
1 对象的格式化输出
(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__() 是很好的习惯,因为它能简化调试和实例输出。
2 获取类的描述
可以通过__doc__这个特殊字段获取类的描述【即类的注释】,用法 【类名.__doc__】,参见下列示例:
class A: """description...""" def func(self): pass print(A.__doc__) # description...
3 获取类或对象的所有成员
可以通过__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'}
可以看到类与对象成员之中,只有【普通字段】是存储在对象中的,其他成员都是在类中。参见笔者这篇文章:面向对象(二)【类的成员及修饰符】。
4 获取创建当前操作的对象的类名
通过__class__能够获取当前操作的对象是由哪个类所创建,用法【对象.__class__】,参见下例:
class A: def func(self): pass obj = A() # 获取 [当前操作的对象] 所在的类名 print(obj.__class__) # <class '__main__.A'>
5 获取创建当前操作的对象的类所在的模块名
通过__module__能够获取创建当前操作的对象的类所在的模块,用法【对象.__module__】,参见下例:
class A: def func(self): pass obj = A() # 获取 [当前操作的对象] 所在的模块名 print(obj.__module__) # __main__
6 让对象可迭代
只需要在类中实现__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__()方法。
7 让对象支持字典操作
实现__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__
8 优化大量对象占用的内存
如果需要创建大量(成千上万)的对象,导致很占内存,可以通过特殊的静态字段__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__使用的冲动。
9 构造方法
类中的__init__()方法就是类的构造方法了,通过类创建对象时,自动触发执行。参见下例:
class A: def __init__(self, name): self.name = name print("this is __init__") # 创建对象则自动触发__ini__方法。 obj = A("Jeo Chen") # this is __init__
直到此时,才介绍构造方法其实是为后面的内容铺垫。这里说创建对象时自动触发执行构造方法是不准确的,继续往下读,会介绍__init__的真正作用。
10 析构方法
__del__方法即为类的析构方法,当对象在内存中被释放时,自动触发执行。不过,Python是有垃圾回收机制的高级语言,我们无需关心内存的分配和释放。解释器在进行垃圾回收时自动触发执行的析构方法。
class A: def __del__(self): pass
11 __call__方法
当在对象后面加括号,即 【对象()】会自动触发__call__方法,这一点要与构造方法相区别。构造方法是类名后面加括号,即【类名()】触发。
class A: def __call__(self): print("this __call__") obj = A() obj() # 对象后面加括号,触发__call__ # this __call__
12 __new__方法
实际上,在创建对象时,调用__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__方法是初始化对象的,【初始化】的过程也就是将字段封装到对象中,通过 对象.字段 就能访问。
13 类是怎么产生的
常常听到,"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。
本篇完。