面向对象(三)【类的特殊成员及高级特性】

前面两篇文章介绍了类与对象的基本概念和类中的一些成员,本篇主要介绍类和对象的特殊成员及一些高级特性。

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。

本篇完。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏JetpropelledSnake

Python入门之函数的介绍/定义/定义类型/函数调用/Return

 本篇目录:     一、 函数的介绍     二、 函数的定义     三、 定义函数的三种类型     四、 函数调用的阶段     五、 Return返回...

3795
来自专栏大前端_Web

详解javascript作用域和闭包

版权声明:本文为吴孔云博客原创文章,转载请注明出处并带上链接,谢谢。 https://blog.csdn.net/wkyseo/article/deta...

1234
来自专栏老司机的技术博客

人人都能学会的python编程教程8:条件判断与循环

实际的项目中条件判断可以说是使用最多的语法之一了,不管是最简单的判断还是负责的业务逻辑和算法,条件判断都如影随形。

1.2K10
来自专栏从零开始学自动化测试

python笔记22-literal_eval函数处理返回json中的单双引号

在做接口测试的时候,最常见的接口返回数据就是json类型,json类型数据实际上就是字串,通常标准的json格式是可以转化成python里面的对应的数据类型的 ...

1991
来自专栏思考的代码世界

Python基础学习00天

1233
来自专栏IT可乐

Redis详解(五)------ redis的五大数据类型实现原理

  前面两篇博客,第一篇介绍了五大数据类型的基本用法,第二篇介绍了Redis底层的六种数据结构。在Redis中,并没有直接使用这些数据结构来实现键值对数据库,而...

1410
来自专栏技术博客

Knockout.Js官网学习(数组observable)

  如果你需要探测和响应一个集合对象的变化,你应该用observableArray 。

1604
来自专栏老司机的技术博客

宝宝都能学会的python编程教程8:条件判断与循环

先公布上期编程练习的答案,没错,L是一个指向三个列表的二维元祖。 条件判断 实际的项目中条件判断可以说是使用最多的语法之一了,不管是最简单的判断还是负责的业务逻...

3535
来自专栏DannyHoo的专栏

Copy mutableCopy 深拷贝、浅拷贝

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u010105969/article/details/...

1283
来自专栏恰童鞋骚年

你必须知道的指针基础-2.指针的声明和使用及数组和指针的关系

  At first,计算机中绝大部分数据都放到内存中的,不同的数据放到不同的内存区域中。But,内存角度没有数据类型,只有二进制;数据以字节(8位二进制)为单...

481

扫码关注云+社区

领取腾讯云代金券