面向对象编程

基本思想

OOP把对象作为程序的基本单元, 一个对象包含了数据和操作数据的函数面向过程的程序设计将计算机程序视为一系列命令的集合, 即一组函数的顺序执行; 而面向对象的程序设计将计算机程序视为一组对象的集合, 而每个对象都可以接收其它对象发过来的消息, 并处理这些消息, 程序的执行就是一系列消息在各个对象之间传递

基础概念

  • 类(classs): 自定义的对象数据类型, 一类对象的抽象模板

  • 方法(Method): 对象对应的关联函数

  • 实例(Instance): 由抽象模板生成的具体对象所以, 面向对象的设计思想是抽象出Class, 根据Class创建Instance

  • 类变量: 定义在类中且在函数体之外的变量; 类变量在整个实例化的对象中是公用的

  • 实例变量: 在类的声明中, 属性是用变量类表示的, 这种变量就称为实例变量

  • 局部变量: 定义在方法中的变量, 只作用于当前实例的类(当前对象)

  • 数据对象: 类变量或实例变量用来处理类及其实例对象的相关数据

  • 方法重写: 对从父类继承的方法的重写或覆盖

基础语法

定义通过class关键字, 其后紧接的是类名; 之后可以加括号, 在括号中写另一个类, 表示从该类继承下来, 如无合适的继承类, 就使用object类, 这是所有类最终都会继承的类

定义实例只需要类名+(), 同时我们可以自由地给一个实例变量绑定属性

由于类可以起到模板的作用, 因此可以通过定义一个特殊的__init__的方法, 在创建实例是, 自动绑定一些必须的属性, 也即实例的初始化

实例中方法的第一个参数永远是self(指向实例本身的变量, 变量名理论上随意, 一般取self), 表示创建的实例本身; 但调用方法时, 不需要传入第一个参数, 因为Python解释器会自己将实例变量传入

类的专有方法

  • __init__: 构造函数, 在生成实例时调用
  • __del__: 折构函数, 释放对象时调用
  • __str__: 打印(print()函数的输出)
  • __repr__: 打印, 转换(repr()函数的输出, 或调试时直接输出的变量)
  • __iter__: 对象的迭代(该方法返回一个迭代对象, 然后for循环将会不断调用该对象的__next__()方法, 知道遇到StopIteration退出循环)
  • __setitem__: 按照索引赋值
  • __getitem__: 按照索引获取值
  • __getattr__: 动态获取属性(在类中未找到属性的情况下, 会调用该方法尝试获取该属性, 如在该方法下仍未定义, 则返回None而不是AttributeError异常)
  • __call__: 直接对实例进行调用, 使实例可运行(callable()函数返回值为True)
  • __len__: 获得长度
  • __cmp__: 比较运算
  • __call__: 函数调用
  • __add__: 加运算
  • __sub__: 减运算
  • __mul__: 乘运算
  • __truediv__: 除运算
  • __mod__: 求余运算
  • __pow__: 乘方运算

类中的特殊变量

  • __slots__: 限制实例能添加的属性(元组, 元素为可以添加的属性)[该变量定义的属性仅对当前实例起作用, 对继承的子类不起作用;除非在子类中也定义, 则子类实例允许的属性便是自身定义的加上父类的 ]

封装

在一个类中, 每个实例拥有各自的属性数据, 我们可以通过外部函数来访问这些数据, 但也可以通过类的方法(定义在类内部的函数)直接实现功能, 这样就将数据给封装了起来, 不需要外部函数访问类中的属性数据便可实现对应的功能, 可以更方便的调用功能, 而不需要知道如何实现; 同时, 封装的另一个好处便是可以方便的给类增加新的方法

访问限制

通过数据封装, 我们可以让外部代码不访问内部属性变量的情况下通过类的方法实现功能, 但本质上依然可以访问内部属性变量; 如果想让内部属性不被外部访问, 则可以在属性的名称前加两个下划线__, 此时这个变量将变成一个私有变量(private), 只有内部可以访问如果外部代码需要访问类内的数据并进行修改, 则可以在类内定义访问和修改的方法, 这样可以确保外部代码不能随意修改对象内部的状态, 同时还可以在方法中对参数做检测, 以避免传入无效的参数
: 在Python中, 变量名类似__xxx__的为特殊变量, 是可以直接访问的, 而不是私有变量; 同时, 习惯上_name这样的变量也被用作代表私有变量, 但其本质上外部依然可以访问; 而双下划线开头的变量也并非外部不可以访问, 只是解释器对外把这些变量改为了_classname__privatename, 所以依然可以通过_classname__privatename在外部访问

私有变量(既指实例变量也指类变量局部变量)相似, 也可以通过__将方法变为私有方法

继承

当我们定一个类时, 可以从某个现有的类继承, 新的类称为子类(Subclass), 而被继承的类称为基类, 父类超类(Base class, Super class), 同时子类可将继承父类的字段方法, 同时继承也允许把子类对象作为一个基类对象对待

多继承

即子类可从多个父类继承, 因此一个子类就可以同时获得多个父类的所有功能

语法

1
2
3
4
5
6
class DerivedClassName(Base1, Base2,...):
<statement-1>
.
.
.
<statement-N>

: 子类调用父类的方法时, 将按定义时的父类顺序依次查找, 如果有两个以上的父类拥有相同名称的方法, 则子类调用位置在前的父类方法

MixIn

在设计类的继承关系时, 通常, 主线都是单一继承下来的, 如果需要“混入”额外的功能, 则通过多重继承实现; 这样的设计通常称为MixIn, 可以将混入的功能类命名为功能名MixIn

多态

一个实例的相同方法在不同情形下有不同的表现形式; 如由一个父类派生出的所有子类将拥有一个相同名称的方法, 在调用同一个函数的情况下, 可以通过改变参数类型(条件 — 拥有函数内调用的对象方法的类)便可实现不同的输出; 也即可使不同内部结构的对象共享相同的外部接口

“开闭”原则

对扩展开放: 允许新增子类对修改封闭: 不需要修改依赖父类的函数

静态语言VS动态语言

对于静态语言(如Java)来说, 如果需要传入一个类型, 则传入的对象类型必须为其本身或其子类

而对于动态语言(如Python)来说, 不一定需要传入其本身或其子类, 只需要保证传入的对象有函数内调用的方法即可这便是动态语言的“鸭子类型”, 它并不要求严格的继承体系, 一个对象只要*“看起来像鸭子, 走起路来像鸭子”*, 那它就可以被看作是鸭子

获取对象信息

  1. 判断对象类型, 通过使用type()函数, 一般内置的数据类型和自定义的类都可以判断, 如果要判断一个对象是否是函数, 则可以使用types模块中定义的一些常量来判断
    • types.FunctionType – 自定义函数
    • types.BuiltinFunctionType – 内置函数
    • types.LambdaType – 匿名函数
    • types.GeneratorType – 生成器
  2. 对于类的继承关系, 使用type()不是很方便, 此时便可使用isinstance()函数来判断类的继承关系, 该函数可以判断一个对象是否是该类型本身或位于该类型的父继承链上
  3. dir()函数可以获得一个对象的所有属性和方法, 返回值为一个包含字符串的列表同时, 还可以通过getattr(), setattr()以及hasattr函数来获取, 设置及测试属性或方法

属性(@property)

@property装饰器可以将一个类中的方法作为实例的属性使用, 可以有效解决在封装后外部很难访问内部数据对象的问题

:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Student(object):
@property
def score(self):
return self.__score
@score.setter
def score(self, value):
if not isinstance(value, int):
raise ValueError('score must be an integer!')
if value < 0 or value > 100:
raise ValueError('score must between 0~100')
self.__score = value

a = Student()
a.score = 60
a.score # 输出 60

用法: 将一个getter方法变成属性, 只需加上@property装饰器即可, 同时@property本身又创建了另一个装饰器@getter方法名.setter, 负责将一个setter方法变成属性赋值(如果只定义getter方法, 而不定义setter方法就是一个只读属性)

: 方法的属性名不应与实例变量重名; 如在调用getter方法时, 本意返回实例变量值, 但因实例变量和方法的属性名相同, 则解释器将视为访问转换为属性的方法, 则将造成无限递归, 最终导致栈溢出RecursionError

__call__和__getattr__方法

在一些网站的调用APIURL生成可以使用__getattr__来动态的生成(递归的思想), 效果如下

1
2
>>> API().status.user.timeline.list
'/status/user/timeline/list'

代码如下

1
2
3
4
5
6
7
8
class API(object):
def __init__(self, path=''):
self.__path = path
def __getattr__(self, path):
return API('{0}/{1}'.format(self.__path, path))
def __str__(self):
return self.__path
__repr__ = __str__

如果想实现输入API().users('michael').repos, 输出/users/michael/repos的效果, 则只需添加如下方法:

1
2
def __call__(self, path):
return Test('{0}/{1}'.format(self.__path, path))

枚举类

Python中并没有提供常量元素的定义, 所以在定义类似C中的枚举类型时, 仍然定义的是变量; 为解决此问题, Python提供了Enum类来实现枚举

:

1
2
3
4
5
6
7
8
9
from enum import Enum
Month = Enum('Month', ('Jan', 'Feb', 'Mar', 'Apr', ...))

Month.Jan # 1

for name, member in Month.__members__.items():
print('{0} => {1}, {2}'.format(name, member, member.value))
# Jan =>1
# ...

元类

type()

除了用class关键字创建类, 还可以通过type()来创建类(本质上通过class来创建类也是调用type()来创建的)

语法: classname = type('classname', (object,), dict(method=func, attribute=var,...))
参数: <classname\> – 类名; <superclass list\> – 父类元组; <contenet dict\> – 属性或方法字典
返回值: 定义好的类

metaclass

除了使用type()动态创建类以外, 还可以通过元类来创建

正常使用类的流程: 先定义类, 然后创建实例; 而元类的使用: 先定义metaclass(元类), 然后创建类, 最后创建实例

如要给一个元类的子类增加一个方法, 则解释器会调用元类的__new__()方法来增加

语法: def __new__(cls, name, bases, attrs):
参数: <cls\> – 当前准备创建的类的对象(也即实例); <name\> – 类的名字; <bases\> – 类继承的父类的集合(元组); <attrs\> – 类的方法集合(字典)