zoukankan      html  css  js  c++  java
  • 面向对象(三)【类的特殊成员及高级特性】

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

    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。

    本篇完。

     

  • 相关阅读:
    POJ3259 Wormholes
    leetCode-Plus One
    leetCode-Pascal's Triangle
    leetCode-Longest Continuous Increasing Subsequence
    leetCode-Missing Number
    leetCode-Maximum Product of Three Numbers
    leetCode-Image Smoother
    leetCode-Contains Duplicate
    机器学习实战笔记-使用Apriori算法进行关联分析
    leetCode-Degree of an Array
  • 原文地址:https://www.cnblogs.com/zingp/p/8583093.html
Copyright © 2011-2022 走看看