python 对象编程

对象编程

  1. 轻松看待对象

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    //对象的来历
    了解对象,先了解类(class)和对象(object)

    在面向过程编程中,函数和模块提高了程序的可复用性.
    在面向对象编程中,类和对象同样提交了程序的可复用性,除此之外,类和对象这两种语法结构还加强了程序模拟真实世界的能力."模拟"正式面向对象编程的核心.

    python允许程序员以纯粹的面向过程的方式来使用它,所以人们有时会忽略它那颗面向对象的心.
    python的一条哲理理念是"一切皆对象"

    //类
    class Bird(object):
    feather = True
    reproduction = "egg"

    def chirp(self,sound):
    print(sound)

    python使用关键字class来定义类,冒号和缩进说明了属于这个类的代码
    python将一些静态描述信息定义为类的属性;将一些"行为"属性定义为方法(方法是在类内部定义函数来说明)

    //对象
    通过调用类,我们可以创造出这个类下面一个对象

    summer = Brid()
    说明summer是属于鸟类的一个对象,作为鸟类的对象summer将拥有鸟类的属性和方法

    对属性的引用:
    print(summer.reproduction)

    对方法的调用:
    print(summer.chirp('jijiji'))

    在调用方法时,我们传递了一个参数('jijiji'),这正是方法和函数的区别
    尽管在定义类的方法时,我们必须加上self参数,但self只能用在类定义的内部,所以在调用方法时不需要对self传入参数

    为了完整描述个体,除了共性的类属性外,我们还需要用于说明个性的对象属性,我们通过self来操作对象的属性:
    class Bird(object):
    feather = True
    reproduction = "egg"

    def chirp(self,sound):
    print(sound)

    def set_color(self,color):
    self.color = color

    summer = Bird()
    summer.set_color('red')
    print(summer.color)

    由于对象属性依赖于self,所以我们必须在某个方法内部才能操作类属性.因此,对象属性没办法像类属性一样,在类下方直接赋初始值


    python定义了一系列特殊方法用于初始化对象属性.我们称为魔法方法
    例如,__init__()方法,python会在每次创建对象时自动调用,因此可以在__init__()方法内部来初始化对象属性
    class Bird(object):
    def __init__(self,sound):
    self.sound = sound
    print('my sound is ',sound)
    def chirp(self):
    print(self.sound)
    summer = Bird('jijiji')
    summer.chirp()

    self除了操作对象属性外,还可以在一个方法内部调用同一类下的其它方法:
    class Bird(object):
    def chirp(self,sound)
    print(self.sound)
    def chirp_repeat(self.sound,n):
    for i in range(n):
    self.chirp(sound)
    summer = Bird()
    summer.chirp('jijiji',5)
  2. 继承

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    类还可以细分为子类,我们通过继承来表达上述概念
    例如:
    class Bird(object):
    feather = True
    reproduction = 'egg'
    def chirp(self,sound):
    print(self.sound)

    class Chicken(Bird):
    how_to_move = 'walk'
    edible = True

    class Swan(Bird):
    how_to_move = 'swim'
    edible = False

    summer = Chicken()
    print(summer.feather)
    summer.chirp('haha')

    在类定义时,括号里为Bird,这说明,鸡类是属于鸟类的一个子类,即Chicken继承自Bird
    我们可以通过继承来减少程序中重复信息和重复语句
    面向对象语言及其继承机制,正式模拟人的意识分类过程

    在继承过程中,我们可以在子类中增加父类不存在的属性,从而增强子类功能.此外,还可以在子类中替换父类已经存在的属性
    例如:
    class Bird(object):
    def chirp(self):
    print('make sound')
    class Chicken(Bird):
    def chirp(self):
    print('ji')
    bird = Bird()
    bird.chirp()
    chicken = Chicken()
    chicken.chirp()

    在继承过程中,我们可以通过super关键字在子类中调用父类被覆盖的方法
    class Bird(object):
    def chirp(self):
    print('make sound')
    class Chicken(Bird):
    def chirp(self):
    super().chirp()
    print('ji')
    bird = Bird()
    bird.chirp()
    chicken = Chicken()
    chicken.chirp()
  3. 重新来看对象

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    //列表对象
    a = [1,2,3,4,5]
    我们知道a属于list类型,也就是列表类型.其实,所谓的类型就是对象所属的类的名字
    当我们新建一个表时,实际上是在创建list类的一个对象

    type()查看类型
    dir()查询一个类或对象的所有属性
    help()查询函数的说明文档
    pass是python中一个特殊的关键字,用于说明在该语法结构中"什么都不做",这个关键字保持了程序结构完整性

    //元组和字符串对象
    a = (1,2,3)
    元组与列表一样都是序列,但元组不能变更内容.因此,元组只能进行查询操作.

    b = "abc"
    字符串是特殊的元组.字符串有一些方法可以改变字符串,这些方法并不是修改字符串对象,而是删除原有字符串,再建立一个新的字符串,所以并没有违背元组不可变性

    //字典对象
    字典同样是一个类
  4. 意想不到的对象

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    //循环对象
    循环对象是一个包含__next__()方法的对象,这个方法的目的是生成循环的下一个结果.在生成过循环的所有结果之后,该方法将抛出StopIteration异常

    通过iter()方法将一个列表转换为循环对象:
    a = iter([1,2])
    a.__next__() 显示1
    a.__next__() 显示2
    a.__next__() 显示StopIteration异常

    或者:
    for item in iter([1,2]):
    print(item)
    for结构自动调用__next__()方法,将该方法的返回值赋值给item

    相对于序列,循环对象的好处在于: 不用在循环还没有开始的时候,就生成要使用的元素.所有要使用的元素在循环过程中逐渐生成.这样,不仅节省空间,提高效率,还使编程更加灵活

    我们可以借助生成器来自定义循环对象,例如:
    def gen():
    a = 100
    yield a
    a = a*8
    yield a
    yield 1000
    for i in gen():
    print(i)

    python中的range()同样是一个循环对象,而不是一个序列

    //函数对象
    任何一个有__call__()方法的对象都被当做是函数
    例如:
    class Some(object):
    def __call__(self,a):
    return a + 5
    add_five = Some()
    print(add_five(2))

    //模块对象
    python中的模块对应一个.py文件.模块也是对象.
    例如:直接引用标准库中的模块time
    import time
    print(dir(time))

    导入模块的三种方法:
    import time
    import time as t
    from time import sleep

    可以将功能相似的模块放在同一个文件夹下,构成一个模块包,比如:
    mkdir this_dir
    touch this_dir/__init__.py
    import this_dir.module

    注意:
    * 每个模块都有一个__name__()属性,用于记录模块的名字
    * 当一个.py文件作为主程序运行时,这个文件也会有一个对应的模块对象.但这个模块对象的__name__属性会是__main__

    //异常对象
    可以在程序中加入异常处理try结构,用于捕捉程序中出现的异常
    try:
    ...
    except xxxx as e:
    ...

    利用except...as...的语法,我们在except结果中用e来代表捕获到的类型对象
  5. 一切皆对象

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    //运算符
    list是列表的类,dir(list)查看列表属性,能看到一个属性是__add__().从样式上看,__add__()是特殊方法,它定义了"+"运算对于列表对象的意义,两个list对象相加时,会进行合并列表操作.
    print([1,2,3]+[4,5,6]) 等价于 [1,2,3].__add__.[4,5,6]

    默认列表是不能进行减法操作的,我们可以定义"-"操作符,例如:
    class SuperList(list):
    def __sub__(self,b):
    a = self[:]
    b = b[:]
    while len(b) > 0:
    element_b = b.pop()
    if element_b in a:
    a.remove(element_b)
    return a

    print(SuperList([1,2,3]) - SuperList[3,4])

    //元素引用
    a = [1,2,3,4,5,6]
    print(a[3])

    上面程序运行到a[3]时,python发现并理解[]符号,然后调用__getitem__()方法.还有其他特殊函数__setitem__(),__delitem__()等

    //内置函数的实现
    与运算符类似,很多内置函数也都是调用对象的特殊方法,比如:
    len([1,2,3]) 等价于 [1,2,3].__len__()
  6. 属性管理

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    //属性覆盖的背后
    我们在继承中,提到了python中属性的覆盖机制.为了深入理解属性覆盖,我们有必要理解__dict__属性.
    当我们调用对象的属性时,这个属性可鞥呢有很多来源.除了来自对象属性和类属性,这个属性还可能是从祖先类那里继承来的.
    一个类或对象拥有的属性,会记录在__dict__中.这个__dict__是一个词典,键为属性名,对应的值为某个属性.python在寻找对象属性时,会按照继承关系一次寻找__dict__.

    对象属性是分层管理的.对象包含了所有属性,当我们调用某个属性时,python会一层层向下遍历,直到找到那个属性为止
    某个属性可能在不同层被重复定义,python在向下遍历过程中,会选取先遇到的那一个,这正是属性覆盖的原理所在
    子类的属性比父类的同名属性有优先权,这正是属性覆盖的关键
    python在为属性赋值时,只会搜索对象本身的__dict__,如果找不到对应属性,则将在__dict__中添加

    //特性
    同一个对象的不同属性之间可能存在依赖关系.
    当某个属性被修改时,我们希望依赖于该属性的其它属性也同时变化,这时,我们不能通过__dict__的静态词典方式来存储属性
    python提供了多种即时生成属性的方法,其中一种称为特性.特性是特殊的属性.
    特性使用内置函数property()来创建,property()最多可以加载四个参数:前三个参数为函数,分别用于设置获取,修改和删除特性时,python应该执行的操作.最后一个参数为特性的文档,可以为一个字符串,起说明作用
    例如:
    class num(object):
    def __init__(self,value):
    self.value = value
    def get_neg(self):
    return -self.value
    def set_neg(self,value):
    self.value = -value
    def del_neg(self):
    print("value also deleted")
    del self.value
    neg = property(get_neg,set_neg,del_neg,"I'm negative")

    x = num(1.1)
    print(x.neg)
    x.neg = -22
    print(x.neg)
    print(x.value)
    print(num.neg.__doc__)

    //__getattr__()方法
    除了内置函数property外,我们还可以使用__getattr__(self,name)来查询即时生成的属性
    当我们调用对象的一个属性时,如果通过__dict__机制无法找到该属性,那么python就会调用对象的__getattr__()方法,来即时生成该属性
    例如:
    class Bird(object):
    feather = True


    class Chicken(Bird):
    fly = False
    def __init__(self,age):
    self.age = age
    def __getattr__(self,name):
    if name == "adult":
    if self.age > 1.0:
    return True
    else:
    return False
    else:
    raise AttributeError(name)

    summer = Chicken(2)
    print(summer.adult)
    summer.age = 0.5
    print(summer.adult)
    print(summer.test)

    __getattr__()可以将所有的即时生成属性放在一个函数中处理.
    __getattr__()可以根据函数名区别处理不同的属性
    __getattr__()只能用于查询不在__dict__系统中的属性

    __setattr__()用于修改属性
    __delattr__()用于删除属性
  7. 动态类型

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    //动态类型
    动态类型是python的另一个重要核心概念.
    python的变量不需要声明,在赋值时,变量可以重新赋值为其它任意值
    例如: a = 1 整数1是一个对象,对象的名字是a.
    综上: 对象名其实是指向对象的一个引用

    对象是存储在内存中的实体,但我们不能直接接触到该对象.对象名是指向这一对象的引用,借着引用操作对象.
    通过内置函数id()我们能查看到引用指向的是哪个对象,id()返回对象的编号
    除了可以直接打印id外,我们还可以用is运算来判断两个引用是否指向同一个对象

    //可变和不可变对象
    一个对象可以有多个引用
    例如:
    a = 5
    print(id(a))
    b = a
    print(id(b))
    a = a + 2
    print(id(2))

    在操作列表对象时,如果通过元素引用改变了某个元素,那么列表对象自身会发生改变,这种自身能发生改变的对象,称为可变对象.但像整数,浮点数和字符串,则不能改变对象本身.赋值最多只能改变引用的指向,这种对象称为不可变对象

    //从动态类型看函数的参数传递
    函数的参数传递,本质上传递的是引用

    例如:
    >>> def f(x):
    ... print(id(x))
    ... x = 100
    ... print(id(x))
    ...
    >>> a = 1
    >>> print(id(a))
    6806632
    >>> f(a)
    6806632
    6808240
    >>> print(id(a))
    6806632

    参数x是一个新的引用,当我们调用函数f时,a作为数据传递给函数,因此x会指向a所指向的对象,也就是进行一次赋值操作.
    如果a是不可变对象,那么引用a和x之间相互独立,即对参数x的操作不会影响引用a
    如果a是可变对象,那么引用x的操作会对引用a产生影响,即通过一个引用操作可变对象,会影响到其它引用.
    例如:
    >>> def f(x):
    ... x[0] = 100
    ... print(x)
    ...
    >>> a = [1,2,3]
    >>> f(a)
    [100, 2, 3]
    >>> print(a)
    [100, 2, 3]
    a指向了一个可变列表.在函数调用时,a把指向传递给了参数x.这时,a和x两个引用都指向了同一个可变列表.当函数对内部列表进行操作,会被外部的引用a看到
  8. 内存管理

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    //引用管理
    python是一种动态类型的,面向对象的语言内存管理方式

    对象内存管理是基于对象引用的管理!!!

    在python中,引用与对象分离: 一个对象可以有多个引用,而每个对象中都存有指向该对象的引用总数,即引用计数

    引用计数可以通过sys包中的getrefcount()来查看,例如:
    >>> from sys import getrefcount
    >>> a = [1,2,3]
    >>> print(getrefcount(a))
    2
    >>> b = a
    >>> print(getrefcount(a))
    3
    注意: getrefcount所得到的结果会比预期多1


    //对象引用对象
    可变对象(列表,字典)等它们都是数据容器对象,可以包含多个对象
    容器对象中包含的并不是元素对象本身,而是指向各个元素对象的引用

    当一个对象a被另一个对象b引用时,a的引用计数将增加1
    例如:
    >>> from sys import getrefcount
    >>> a = [1,2,3]
    >>> print(getrefcount(a))
    2
    >>> b = [a,a]
    >>> print(getrefcount(a))
    4

    容器对象的引用会构成很复杂的拓扑结构,可以使用objgraph包来绘制其引用关系
    例如:
    >>> import objgraph
    >>>
    >>> x = [1,2,3]
    >>> y = [x,dict(key=1)]
    >>> z = [y,(x,y)]
    >>>
    >>> objgraph.show_refs([z],filename="ref_top.png")
    Graph written to /tmp/objgraph-YJf5mO.dot (9 nodes)
    Image generated as ref_top.png

    两个对象可能相互引用,从而构成引用环.即时单个对象,自己引用自己,也能构成引用环
    例如:
    >>> aa = []
    >>> bb = [aa]
    >>> aa.append(bb)
    >>> objgraph.show_refs([aa],filename="aa_top.png")
    Graph written to /tmp/objgraph-odsHZ9.dot (2 nodes)
    Image generated as aa_top.png

    >>> cc = []
    >>> cc.append(cc)
    >>> objgraph.show_refs([cc],filename="cc_top.png")
    Graph written to /tmp/objgraph-Wtvf78.dot (1 nodes)
    Image generated as cc_top.png

    引用环会给垃圾回收机制带来很大的麻烦

    使用del关键字删除某个引用,也可以用于删除容器中的元素
    例如:
    >>> from sys import getrefcount
    >>> a = [1,2,3]
    >>> b = a
    >>> print(getrefcount(b))
    3
    >>> a = 1
    >>> print(getrefcount(b))
    2


    //垃圾回收
    当python中的对象越来越多时,它们占据越来越大的内存,它会在适当的时候启动垃圾回收,用于清理没用的对象
    原理上,当python的某个对象的引用计数为0时,即没用任何引用指向该对象时,该对象就称为要被回收的垃圾了.当垃圾回收启动时,python扫描到这个引用计数为0的对象,就会将它所占据的内存清空.

    当python运行时,会记录其中"分配对象"和"取消分配对象"的次数.当两者的差值高于某个阀值时,垃圾回收才会启动
    我们可以通过gc模块的get_threshold()方法看到该阀值:
    >>> import gc
    >>> print(gc.get_threshold())
    (700, 10, 10)

    700 垃圾启动的阀值,通过gc的set_threshold()重新设置
    10 每10次0代回收,会配合1次1代的垃圾回收,此时清理0,1代
    10 每10次1代回收,会配合1次2代的垃圾回收,此时清理0,1,2代

    也可以手动回收,通过gc模块的collect()方法

    python除了上面的基础回收外,还同时采用了分代回收策略:存活的时间越久,越不可能在后面的程序中变成垃圾.
    python将所有的对象分为0,1,2三代:
    所有的新建对象都是0代对象,当某一代对象经历过垃圾回收,依然存活,那么它就进入下一代对象.垃圾回收启动时,一定会扫描所有的0代对象.如果0代对象经过一定次数垃圾回收,那么就启动对0代和1代的扫描清理.当1代也经历了一定次数的垃圾回收后,就会启动对0,1,2代的扫描,即对所有对象进行扫描

    //孤立的引用环
    引用环的存在会给上面的垃圾回收机制带来很大的困难
    例如:
    >>> a = []
    >>> b = [a]
    >>> a.append(b)
    >>> del a
    >>> del b

    由于引用环的存在,这两个对象的引用计数都没有将到0,所以不会被垃圾回收.
    为了回收这样的引用环,python会复制每个对象的引用计数,可以标记为gc_ref.
    假设,每个对象i,该计数为gc_ref_i.python会遍历所有对象i,对于每个对象i所引用的对象j,将相应的gc_ref_j减1,遍历后的结果gc_ref为0,进行垃圾回收.如果不为0,则对象被保留