Python核心编程读书笔记--面向对象编程

面向对象编程

1. 类的定义

一个例子如下:


# 代码段 1
class Child(Parent):
     'description for class Child'
     def __init__(self, name, age):
         self.name = name
         self.age = age
         Child.all_childs.append(name)
     code_version = 1
     all_childs = []

2. 经典类与新式类

Python的“类”有两种,即经典类和新式类。

区别
  • Python 2.2 之前的类实现是经典类,Python 2.2之后同时有新式类和经典类。
  • 新式类比新式类多了很多属性(特性),定义一个空的经典类,新式类之后,用dir(类名)的方法就会发现空的经典类只有__doc__, __module__两个类属性,而新式类有10+个类属性。
  • 在有多层继承关系的情况下,经典类搜索类方法的方法是深度优先,而新式类是广度优先(这个后面会再讲)。
识别
  • 经典类:如果定义的时候没有指定父类,或者父类都是经典类,那么该类就是经典类。
  • 新式类:定义的时候指定了父类,且父类中至少有一个是新式类。新式类的根为object,即object也是一个新式类。

    #经典类
    def classicA:
        pass
    def classicB(classicA):
        pass
    #新式类
    def newA(object):
        pass
    def newB(newA, classicA):
        pass
    

3. 类属性和实例属性

类定义中的数据属性可以分为类属性和实例属性。

实例属性
  • 访问:即隶属于实例的数据属性,在类的函数中用self.attr来访问,self即实例本身的引用,通常是在__init__函数中进行初始化(如代码段1);类外通过实例名加句点访问。
  • 动态添加:和在类外使用变量类似,Python支持无需在类中事先声明变量,而在类外直接通过实例名+句点的方式动态添加实例属性。如代码段1中的Child类并没有attr属性,但是c = Child(), c.attr = 'dynamic'是允许的 -- 其实可以将Python的类看做一个特殊的命名空间。
类属性
  • 类属性的定义:如代码段1中的code_version,与类的函数定义同级。
  • 类属性的访问:可以通过类名加句点的方式(Child.code_version)访问,也可以通过实例名加句点的方式(c.code_version)访问; 在类的函数中也要通过类名进行访问(不然会认为是一般的局部变量,而非类属性).
  • 类属性的更新:通过类名加句点访问并进行修改更新;通过实例名加句点的方式进行更新虽然语法不报错——但是假如类属性是诸如int,str,tuple这些不可变类型,那么其结果就是创建了同名的实例属性,并进行更新。

    # 类属性是不可变的--通过实例名更新会创建同名的实例属性
    c = Child('Alice', 18)
    c.code_version = 1.2
    print Child.code_version (结果:1)
    print c.code_version(结果:1.2)
    del c.code_version
    print c.code_version (结果:1,删除了覆盖类属性的实例属性,类属性重见天日)
    
    # 类属性是可变的,如list, dict, 通过实例名更新形同通过类名更新
    print Child.all_childs (结果 ['Alice'])
    c.all_childs.append('Bob')  (结果['Alice', 'Bob']
    
  • 特殊的类属性:__name__(类名),__doc__(文档字符串,即class Child下一行的description文本), __bases__(直接父类), __dict__(属性及其值,包括函数),__module__(所在模块),__class__(实例对应的类 -- 仅新式类中)

4. 类的函数

绑定与方法调用

首先回顾下类的方法的定义和调用

class A(object):
    'description for class A'
    def fun(self):
        '注意参数 self , 即实例的引用'
        pass
a = A()
a.fun()

上面的方法定义时,第一个参数为self,即实例本身的引用,调用函数的时候,是通过实例名加句点的方式进行访问,不必再传self参数,这时候就称方法fun绑定到了实例a身上。

因此所谓的绑定即:通过实例名调用类的函数时,函数就绑定到了该实例身上,不必再显式地传入实例引用,而方法就能访问该实例的属性。

一种非绑定的方式调用函数的场景是:在子类的构造函数中调用父类的构造函数,此时我们不希望生成一个父类的实例,而是传入子类的实例,而由父类构造函数对该子类实例完成父类那部分的初始化工作,此时没有父类实例,就只能通过父类名加句点的方式进行访问,然后显式地传入子类实例,如下

class A(object):
    def __init__(self, name):
        self.name = name
class B(A):
    def __init__(self, name, age):
        # 在方法没有被绑定到某一个实例的情况下调用函数--通过类名调用,显式传入实例
        A.__init__(self, name)
        self.age = age
方法的种类

类的方法分为三种,一般方法,静态方法和类方法。

一般方法

特点:第一个参数是self

调用: 一般通过实例名进行访问,也可以非绑定的方式调用,但要传入实例。

静态方法

特点:没有要求第一个参数是啥;最后用 staticmethod函数转化为静态函数,或者用staticmethod装饰器装饰,因为没有传入self实例,所以固然是不能访问实例属性的,如下。

class A:
     def foo():
         print 'foo'
         foo = staticmethod(foo)
或者
class A:
     @staticmethod
     def foo():
         print 'foo'

调用:通过类名或者实例名访问都可以

类方法

特点:第一个参数是类,参数名可以随便取,通常用cls(class的缩写);最后用 classmethod函数转化为静态函数,或者用classmethod装饰器装饰,因为没有传入self实例,所以固然是不能访问实例属性的,如下。

class A:
     def foo():
         print 'foo'
         foo = classmethod(foo)
或者
class A:
     @classmethod
     def foo():
         print 'foo'

调用:通过类名或者实例名调用都可以。

特殊函数——构造与析构
  • “构造”函数:__init__,其实不同C++中的构造函数,实际上是实例已经有了之后调用的第一个函数,做一些初始化的工作;不许任何返回值,即返回None.
  • “析构”函数:__del__, 删除实例的一个引用,由于Python的垃圾回收机制,当引用数减为0的时候也不会马上销毁对象,而是等到垃圾回收的时候才真正销毁对象。
  • “构造”函数2:__new__, 这个更像C++中的new, 函数的最后必须返回一个实例(构造一个实例并返回),如果同时定义了__new__和__init__,那么会先调__new__然后再调用__init__; __new__是类函数,也即第一参数必须是cls.

5. 子类和派生

创建子类
class Child(Parent):
    'definition for class Child'
    def __init__(self,name,age):
       # 重写了__init__的话就不会自动调用的__init__,需要手动调用
       Parent.__init__(name)
       /* 也可以通过 super(Child, self)来获取父类--这样修改父类名的时候这里就不用改
          super(Child,self).__init__(name)
       */

可以通过__bases__查看直接父类。

继承与覆盖

子类继承父类的方法、属性,同时可以重写,注意如果重写了__init__, 就不会自动调用的__init__,需要手动调用。

从标准类型继承

Python支持从标准类型如int, str派生,其实他们的实现也是类,和其他的类并无不同

class RoundFloat(float):
     def __new__(cls, val):
         return float.__new__(cls, round(val, 2))
多重继承

Python支持多重继承,即同时有多个直接父类,当然也支持多层继承,即有间接父类。 当多个父类或者不同层次的父类有同名函数,而当前类没有重写该函数时,其搜索方法(称为方法解释顺序MRO)是:

经典类按照深度优先进行搜索。

新式类按照广度优先进行搜索。

5. 类相关的内建函数

  • issubclass(sub, sup)
  • isinstance(obj1, cls): obj1是否是cls或者cls的子类的实例
  • hasattr(cls or obj, attr): 判断类、实例是否有属性
  • getattr(cls or obj, attr): 获取属性值
  • setattr(cls or obj, attr): 设置属性值
  • delattr(cls or obj, attr): 删除属性
  • dir(cls or obj): 对于类,返回类属性、函数;对于实例,返回类属性、函数、实例属性。
  • super(cls [, obj]): 返回父类
  • vars(obj): 返回的值同__dict__, 因此给定的对象必须有__dict__属性。

6. 特殊方法定制类

其实主要就是通过实现各种特殊方法来使得类能够适用于各种运算符,如+-*/, +=, [], [:]等,>, in等,常见的包括。

  • 实现__str__函数来使得 print obj 和 str(obj) 输出有意义的内容
  • 实现__repr__函数来支持 repr(obj)和`obj`。通常可以直接令__repr__=__str__
  • 实现__add__函数来支持+,类似还有-*/.
  • 实现__iadd__函数来支持+=, 类似还有-=, *=, /=等。
  • 实现__len__函数来支持len(obj)
  • 实现__getitem__(self, ind), __setitem__(self,ind,val), __delitem__(self,ind)函数来支持通过数值下表操作对象(像序列一样)。
  • 实现__contains__函数来支持in关键字(if in判断)。
  • 实现__getitem__(self,key), __setitem__(self,key,val),__delitem__来支持通过key访问对象内容(像dict一样)。
  • 实现__iter__, next来支持迭代器,以及 for in的迭代。
  • 其他的看《Python核心编程 第二版》 P367-369吧。

    一个例子
    from random import choice
    class RandSeq(obj):
         def __init__(self, data):
             self.data = data
         def __iter__(self):
             return self
         def next(self):
             return choice(self.data)
    rs = RandSeq()
    i = iter(rs)
    rs.next()
    # 其实下面这个是个死循环,因为RandSeq的next每次都是随机从元组中返回一个数据,总能成功,而不引发StopIteration异常来使迭代终止(因此正常的实现是要有raise StopIteration的边界情况的)
    for item in RandSeq(('a', 'b', 'c')):
        print item
    

7. 私有化

Python并没有像C++中的public,private,proteced的关键字,不过可以为属性提供某种程度的私有化——其实是"伪私有化"。

类属性私有化

如类的属性名以双下划线开头,且不以双下划线结尾(不然诸如__dict__之类的内置属性就没法访问了),如类NumStr的属性__num,类则会对运行时该属性加以混淆,即在属性名前加上“_类名”的前缀,如将NumStr的属性__num重命名为_NumStr__num, 从而使得无法通过.__num来访问属性。

这里的“属性”包括数据属性和函数——也就是说通过双下划线开头来“私有化”类的函数也是可行的。

不过通过._NumStr__num来访问属性是可以的,因此说是伪私有化———不过为属性穿上了混淆的外衣。

模块级私有化

如果模块中(顶级)的函数、属性以单下划线开头,则不能通过from mod import *来导入该属性(函数).

不过直接通过 from mod import _attr是可以的——所以说其实也是“伪私有化”。

#mod.py
_attr = 1
def _fun():
    print 'hide _fun in other modules'

# main1.py
from mod import *
print _attr (出错)
_fun()  (出错)

# main2.py
from mod import _attr, _fun
print _attr (ok)
_fun()  (ok)

8. 新式类的高级特性

__slots__类属性

用于替代__dict__的属性。

__dict__是属于实例的属性,因此,当实例很多的时候,会占用大量的内存。

而__slots__是属于类的属性,是一个元组,保存着类的属性集合,适用的场合是:有很多实例,而且这些实例的属性都是一样的,没有需要给实例动态添加属性的需求,此时在类的定义中加上__slots__限定类的属性集合,那么该类的所有实例都将共用一个__slots__变量,而且每个实例没有__dict__属性了,从而节省了大量内存。

class A(object):
    __slots__ = ('foo', 'bar')
a = A()
b = A()
print id(a.__slots__) == id(b.__slots__) (输出:True)
print a.__dict__ (输出AttributeError, 无此属性)
描述符

描述符就是一个类,来作为对象属性的代理,也可以理解为针对对象的某一个属性来重载“.”运算符,使得访问改属性的时候,实际上访问描述符这个类的特定方法,从而达到了对属性访问的控制。

__get__, __set__, __delete__特殊方法

描述符是一个类,实现了__get__, __set__, __delete__中的至少一个函数,用来“描述”某一个属性,这三个函数的作用是:

  • __get__: 通过"."访问属性值的时候调用该函数。
  • __set__: 通过"."访问并给属性赋值的时候调用该函数。
  • __delete__: 通过del 删除属性的时候调用该函数。

举个例子:

# 用这个描述符来“描述”属性,则属性的值恒为None, 对属性的赋值无效
class NullAttr(object):
    def __get__(self, obj, type=None):
        print 'return None when accessing attribute'
        pass
    def __set__(self, obj, val):
        print 'do nothing when assigning value to attribute'
        pass
class A:
    foo = NullAttr()
a = A()
print a.foo() (调用__get__返回None)
a.foo = 'abc' (调用__set__,啥也没做,直接pass)
print a.foo() (还是None)
__getattribute__特殊方法

这是描述符系统的心脏,其实,我们访问新式类的属性(不管是否是描述符)的时候,都会调用__getattribute__函数,如果属性是一个描述符的话,__getattribute__内部会再调用描述符的__get__,__set__函数等。

举个例子, 类A的属性foo为实现了__get__的描述符,a = A(), a.foo在__getattribute__中会被转化为:

type(a).__dict__['foo'].__get__(a, type(a))

__getattribute__内部搜索属性的顺序(优先级)是:

  1. 数据描述符(类属性)
  2. 实例属性
  3. 非数据描述符(类属性)
  4. 通过__getattr__所访问的属性:即在当前类及其父类中找不到属性的定义时,会通过函数__getattr__去搜寻该属性——一个应用场景是,不是类的属性,将类中的某一个对象(属性)的属性授权给当前对象,使得能够通过"."访问这些属性。
属性和property()内建函数

属性是一种有用的特殊类型的描述符。

使用property可以使我们控制属性的获取、赋值和删除操作。property内建函数有四个参数,即:

property(fget=None, fset=None, fdel=None, doc=None)

可以理解为针对这个被property修饰的属性“重载"." ,"=",和“del”运算符。

两种实现方式:

class HideX(object):
    def __init__(self, x):
        assert isinstance(x, int), \
            '"x" must be an integer!'
        # 这里通过.访问赋值x,就会调用set_x, 使得self.__x = ~x, 可以将__x看做是类内部保存的加密(混淆)版的"x"
        self.x = x
    def set_x(self, x):
        self.__x = ~x
    def get_x(self):
        return ~self.__x
    x = property(get_x, set_x)

上面通过取反,将x隐藏保存在实例内部(__x,而且前面讲过双下划线开头的属性本身也提供某种程度的私有化,即属性名加类名前缀以混淆隐藏)。

这种方法的弊端在于:将描述符方法定义为类方法,这个会污染类的命名空间——因为,实际上我们只是希望在描述符中使用这些方法,不希望在类外(通过类名、实例名加句点)使用这些方法。

解决的方法是,借用一个函数(如xfun)的命名空间,将描述符方法定义在函数(xfun)内部,并将描述方法作为函数(xfun)的返回值,使用property将函数转化为一个描述符(函数xfun的返回值--描述符方法为property的输入值),如下。

class HideX(object):
    def __init__(self, x):
        self.x = x
    # 其实下面的函数命名成x更常见些,命名成xfun只是想证明这个名字可以随便取
    def xfun():
        def fget(self):
            return ~self.__x
        def fset(self, x):
            assert isinstance(x, int), \
                '"x" must be an integer!'
            self.__x = ~x
        # 返回一个字典,['fget':.., 'fset':..]作为property()的参数
        return locals
    x = property(**xfun())

其实书里说的是可以不用 x = property(**xfun()), 而是在函数xfun上加上装饰器@property, 不过函数就不能随便命名,因此装饰器是将函数修饰为一个同名的property, 因此还是得将xfun重命名为x,才能正常地访问属性x。

But 这种通过装饰器property来定义属性描述符的方法我没有测试通过。。。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值