面向过程的程序设计的核心是过程(流水线式思维),过程即解决问题的步骤,面向过程的设计就好比精心设计好一条流水线,考虑周全什么时候处理什么东西。
- 优点是:极大的降低了程序的复杂度;
- 缺点是:可扩展性差,修改代码麻烦;
- 应用场景:一旦完成基本很少改变的场景,著名的例子有Linux內核,git,以及Apache HTTP Server等。
面向对象的程序设计的核心是对象(上帝式思维),要理解对象为何物,必须把自己当成上帝,上帝眼里世间存在的万物皆为对象,不存在的也可以创造出来。面向对象的程序设计好比如来设计西游记,如来要解决的问题是把经书传给东土大唐,如来想了想解决这个问题需要四个人:唐僧,沙和尚,猪八戒,孙悟空,每个人都有各自的特征和技能(这就是对象的概念,特征和技能分别对应对象的数据属性和方法属性),然而这并不好玩,于是如来又安排了一群妖魔鬼怪,为了防止师徒四人在取经路上被搞死,又安排了一群神仙保驾护航,这些都是对象。然后取经开始,师徒四人与妖魔鬼怪神仙交互着直到最后取得真经。如来根本不会管师徒四人按照什么流程去取。
面向对象的程序设计
- 优点:解决了程序的扩展性。对某一个对象单独修改,会立刻反映到整个体系中,如对游戏中一个人物参数的特征和技能修改都很容易。
- 缺点:可控性差,无法向面向过程的程序设计流水线式的可以很精准的预测问题的处理流程与结果,面向对象的程序一旦开始就由对象之间的交互解决问题,无法预测最终结果。于是我们经常看到一个游戏人某一参数的修改极有可能导致bug的技能出现,一刀砍死3个人,这个游戏就失去平衡。
- 应用场景:需求经常变化的软件,一般需求的变化都集中在用户层,互联网应用,企业内部软件,游戏等都是面向对象的程序设计大显身手的好地方。
面向对象的程序设计并不是全部。对于一个软件质量来说,面向对象的程序设计只是用来解决扩展性。
面向对象技术简介
- 类(Class): 用来描述具有相同的属性和方法的对象的集合。它定义了该集合中每个对象所共有的属性和方法。对象是类的实例。
- 类变量:类变量在整个实例化的对象中是公用的。类变量定义在类中且在函数体之外。类变量通常不作为实例变量使用。
- 数据成员:类变量或者实例变量, 用于处理类及其实例对象的相关的数据。
- 方法重写:如果从父类继承的方法不能满足子类的需求,可以对其进行改写,这个过程叫方法的覆盖(override),也称为方法的重写。
- 局部变量:定义在方法中的变量,只作用于当前实例的类。
- 实例变量:在类的声明中,属性是用变量来表示的。这种变量就称为实例变量,是在类声明的内部但是在类的其他成员方法之外声明的。
- 继承:即一个派生类(derived class)继承基类(base class)的字段和方法。继承也允许把一个派生类的对象作为一个基类对象对待。例如,有这样一个设计:一个Dog类型的对象派生自Animal类,这是模拟"是一个(is-a)"关系(例图,Dog是一个Animal)。
- 实例化:创建一个类的实例,类的具体对象。
- 方法:类中定义的函数。
- 对象:通过类定义的数据结构实例。对象包括两个数据成员(类变量和实例变量)和方法。
由于Python是动态语言,根据类创建的实例可以任意绑定属性。
给实例绑定属性的方法是通过实例变量,或者通过self变量。
self代表类的实例,而非类
类名加括号就是实例化,会自动触发__init__函数的运行,可以用它来为每个实例定制自己的特征。
类的方法与普通的函数只有一个特别的区别——它们必须有一个额外的第一个参数名称, 按照惯例它的名称是 self。
实例属性属于各个实例所有,互不干扰;类属性虽然归类所有,但类的所有实例都可以访问到。
在编写程序的时候,千万不要对实例属性和类属性使用相同的名字,因为相同名称的实例属性将屏蔽掉类属性,但是当你删除实例属性后,再使用相同的名称,访问到的将是类属性。
>>> class Student(object): ... name = 'Student' ... >>> s = Student() # 创建实例s >>> print(s.name) # 打印name属性,因为实例并没有name属性,所以会继续查找class的name属性 Student >>> print(Student.name) # 打印类的name属性 Student >>> s.name = 'Michael' # 给实例绑定name属性 >>> print(s.name) # 由于实例属性优先级比类属性高,因此,它会屏蔽掉类的name属性 Michael >>> print(Student.name) # 但是类属性并未消失,用Student.name仍然可以访问 Student >>> del s.name # 如果删除实例的name属性 >>> print(s.name) # 再次调用s.name,由于实例的name属性没有找到,类的name属性就显示出来了 Student
可以使用以下函数的方式来访问属性:
- getattr(obj, name[, default]) : 访问对象的属性。
- hasattr(obj,name) : 检查是否存在一个属性。
- setattr(obj,name,value) : 设置一个属性。如果属性不存在,会创建一个新属性。
- delattr(obj, name) : 删除属性。
Python内置类属性
- __dict__ : 类的属性字典,或者说名称空间
- __doc__ :类的文档字符串
- __name__: 类名
- __module__: 类定义所在的模块(类的全名是'__main__.className',如果类位于一个导入模块mymod中,那么className.__module__ 等于 mymod)
- __bases__ : 类的所有父类构成元素(包含了一个由所有父类组成的元组)
我们定义的类的属性到底存到哪里了?有两种方式查看
dir(类名):查出的是一个名字列表
类名.__dict__:查出的是一个字典,key为属性名,value为属性值
python对象销毁(垃圾回收)
Python 使用了引用计数这一简单技术来跟踪和回收垃圾。
在 Python 内部记录着所有使用中的对象各有多少引用。
一个内部跟踪变量,称为一个引用计数器。
当对象被创建时, 就创建了一个引用计数, 当这个对象不再需要时, 也就是说, 这个对象的引用计数变为0 时, 它被垃圾回收。但是回收不是"立即"的, 由解释器在适当的时机,将垃圾对象占用的内存空间回收。
垃圾回收机制不仅针对引用计数为0的对象,同样也可以处理循环引用的情况。循环引用指的是,两个对象相互引用,但是没有其他变量引用他们。这种情况下,仅使用引用计数是不够的。Python 的垃圾收集器实际上是一个引用计数器和一个循环垃圾收集器。作为引用计数的补充, 垃圾收集器也会留心被分配的总量很大(及未通过引用计数销毁的那些)的对象。 在这种情况下, 解释器会暂停下来, 试图清理所有未引用的循环。
析构函数 __del__ ,__del__在对象销毁的时候被调用,当对象不再被使用时,__del__方法运行
类的继承
面向对象的编程带来的主要好处之一是代码的重用,实现这种重用的方法之一是通过继承机制。
通过继承创建的新类称为子类(Subclass)或派生类,被继承的类称为基类、父类或超类(Base class、Super class)。
继承语法
class 派生类名(基类名)
...
在python中继承中的一些特点:
- 1、如果在子类中需要父类的构造方法就需要显示的调用父类的构造方法,或者不重写父类的构造方法。
- 2、在调用基类的方法时,需要加上基类的类名前缀,且需要带上 self 参数变量。区别在于类中调用普通函数时并不需要带上 self 参数
- 3、Python 总是首先查找对应类型的方法,如果它不能在派生类中找到对应的方法,它才开始到基类中逐个查找。(先在本类中查找调用的方法,找不到才去基类中找)。
如果在继承元组中列了一个以上的类,那么它就被称作"多重继承" 。
语法:
派生类的声明,与他们的父类类似,继承的基类列表跟在类名之后,如下所示:
class SubClassName (ParentClass1[, ParentClass2, ...]): ...
继承的定义 继承是一种创建新类的方式,在python中,新建的类可以继承一个或多个父类,父类又可称为基类或超类,新建的类称为派生类或子类。 分类:单继承和多继承 class ParentClass1: #定义父类 pass class ParentClass2: #定义父类 pass class SubClass1(ParentClass1): #单继承,基类是ParentClass1,派生类是SubClass pass class SubClass2(ParentClass1,ParentClass2): #python支持多继承,用逗号分隔开多个继承的类 pass print(SubClass2.__base__) print(SubClass2.__bases__) print(ParentClass1.__base__) 输出 <class '__main__.ParentClass1'> (<class '__main__.ParentClass1'>, <class '__main__.ParentClass2'>) <class 'object'> 如果没有指定基类,python的类会默认继承object类,object是所有python类的基类,它提供了一些常见方法(如__str__)的实现。
继承有什么好处?最大的好处是子类获得了父类的全部功能。继承的另一个好处:多态。
要判断class的类型,可以使用isinstance()函数。isinstance()判断的是一个对象是否是该类型本身,或者位于该类型的父继承链上。在继承关系中,如果一个实例的数据类型是某个子类,那它的数据类型也可以被看做是父类。
多态真正的威力:调用方只管调用,不管细节,这就是著名的“开闭”原则,对扩展开放:允许新增子类;对修改封闭:不需要修改依赖类的函数。
继承还可以一级一级地继承下来,就好比从爷爷到爸爸、再到儿子这样的关系。而任何类,最终都可以追溯到根类object,这些继承关系看上去就像一颗倒着的树。
Python是动态语言,动态语言的“鸭子类型”,它并不要求严格的继承体系,一个对象只要“看起来像鸭子,走起路来像鸭子”,那它就可以被看做是鸭子。动态语言的鸭子类型特点决定了继承不像静态语言那样是必须的。
方法重写
如果你的父类方法的功能不能满足你的需求,你可以在子类重写你父类的方法:
基础重载方法
下表列出了一些通用的功能,你可以在自己的类重写:
序号 | 方法, 描述 & 简单的调用 |
---|---|
1 | __init__ ( self [,args...] ) 构造函数 简单的调用方法: obj = className(args) |
2 | __del__( self ) 析构方法, 删除一个对象 简单的调用方法 : del obj |
3 | __repr__( self ) 转化为供解释器读取的形式 简单的调用方法 : repr(obj) |
4 | __str__( self ) 用于将值转化为适于人阅读的形式 简单的调用方法 : str(obj) |
5 | __cmp__ ( self, x ) 对象比较 简单的调用方法 : cmp(obj, x) |
运算符重载
类属性与方法
类的私有属性
__private_attrs:两个下划线开头,声明该属性为私有,不能在类的外部被使用或直接访问。在类内部的方法中使用时 self.__private_attrs。
类的方法
在类的内部,使用 def 关键字可以为类定义一个方法,与一般函数定义不同,类方法必须包含参数 self,且为第一个参数
类的私有方法
__private_method:两个下划线开头,声明该方法为私有方法,不能在类的外部调用。在类的内部调用 self.__private_methods
Python不允许实例化的类访问私有数据,但你可以使用 object._className__attrName( 对象名._类名__私有属性名 )访问属性
单下划线、双下划线、头尾双下划线说明:
-
__foo__: 定义的是特殊方法,一般是系统定义名字 ,类似 __init__() 之类的。特殊变量可以直接访问
-
_foo: 以单下划线开头的表示的是 protected 类型的变量,即保护类型只能允许其本身与子类进行访问,不能用于 from module import *,意思就是,“虽然我可以被访问,但是,请把我视为私有变量,不要随意访问”。
-
__foo: 双下划线的表示的是私有类型(private)的变量, 只能是允许这个类本身进行访问了。
使用__slots__
如果我们想要限制实例的属性怎么办?比如,只允许对Student实例添加name和age属性。
为了达到限制的目的,Python允许在定义class的时候,定义一个特殊的__slots__变量,来限制该class实例能添加的属性:
class Student(object): __slots__ = ('name', 'age') # 用tuple定义允许绑定的属性名称 >>> s = Student() # 创建新的实例 >>> s.name = 'Michael' # 绑定属性'name' >>> s.age = 25 # 绑定属性'age' >>> s.score = 99 # 绑定属性'score' Traceback (most recent call last): File "<stdin>", line 1, in <module> AttributeError: 'Student' object has no attribute 'score' 由于'score'没有被放到__slots__中,所以不能绑定score属性,试图绑定score将得到AttributeError的错误。 使用__slots__要注意,__slots__定义的属性仅对当前类实例起作用,对继承的子类是不起作用的: >>> class GraduateStudent(Student): ... pass ... >>> g = GraduateStudent() >>> g.score = 9999
实例方法(instance method),类方法(class method)与静态方法(static method)
实例方法,除静态方法与类方法外,类的其他方法都属于实例方法。
实例方法需要将类实例化后调用,如果使用类直接调用实例方法,需要显式地将实例作为参数传入。
类调用:类名.方法(实例对象名)
对象调用:直接调用(实例对象名.方法)
class Person:
def run(self):
pass
class ClassA(object): def func_a(self): print('Hello Python') # 使用实例调用实例方法 ca = ClassA() ca.func_a() # 如果使用类直接调用实例方法,需要显式地将实例作为参数传入 ClassA.func_a(ca)
类方法传入的第一个参数为cls,是类本身。并且,类方法可以通过类直接调用,或通过实例直接调用。但无论哪种调用方式,最左侧传入的参数一定是类本身。
类方法使用@classmethod装饰器来声明。
class Person:
@classmethod
def countPerson(cls):
pass
类调用:不用手动传递第一个参数,会自动的把调用的类本身给传递过去
对象调用:不用手动传递第一个参数,会自动的把调用的对象对应的类给传递过去
class Person: @classmethod def leifangfa(cls, a): print("这是一个类方法", cls, a) Person.leifangfa(123) p = Person() p.leifangfa(666) # 输出: # 这是一个类方法 <class '__main__.Person'> 123 # 这是一个类方法 <class '__main__.Person'> 666
静态方法是指类中无需实例参与即可调用的方法(不需要self参数),静态方法使用@staticmethod装饰器来声明。
类调用:直接调用(类名.方法)
对象调用:直接调用(实例对象名.方法)
class Person:
@staticemethod
def countPerson():
pass
class ClassA(object): @staticmethod def func_a(): print('Hello Python') if __name__ == '__main__': ClassA.func_a() #类调用 ca = ClassA() ca.func_a() #实例对象调用
class ClassA(object): def func_a(): print('Hello Python') if __name__ == '__main__': ClassA.func_a() ca = ClassA() ca.func_a() 异常信息: func_a() takes 0 positional arguments but 1 was given 因为func_a没有声明为静态方法,类实例在调用func_a时,会隐式地将self参数传入func_a,而func_a本身不接受任何参数,从而引发异常。
小结
1.定义形式上:
a. 类方法和静态方法都是通过装饰器实现的,实例方法不是;
b. 实例方法需要传入self参数,类方法需要传入cls参数,而静态方法不需要传self或者cls参数。
2. 调用方式上:
实例方法只能通过实例对象调用;类方法和静态方法可以通过类对象或者实例对象调用,如果是使用实例对象调用的类方法或静态方法,最终都会转而通过类对象调用。
3. 应用场景:
a. 实例方法使用最多,可以直接处理实例对象的逻辑;类方法不需要创建实例对象,直接处理类对象的逻辑;静态方法将与类对象相关的某些逻辑抽离出来,不仅可以用于测试,还能便于代码后期维护。
b. 实例方法和类方法,能够改变实例对象或类对象的状态,而静态方法不能。
metaclass:创建类对象的类
实例对象由类创建出来,类是一个对象,类由元类创建出来。
metaclass允许创建类或者修改类。可以把类看成是metaclass创建出来的“实例”。
Python解释器遇到class定义时,先扫描class定义的语法,然后调用type()函数创建出class。
要创建一个class对象,type()函数依次传入3个参数:
class的名称;
继承的父类集合,注意Python支持多重继承,如果只有一个父类,别忘了tuple的单元素写法;
class的方法名称与函数绑定。
>>> def fn(self, name='world'): # 先定义函数 ... print('Hello, %s.' % name) ... >>> Hello = type('Hello', (object,), dict(hello=fn)) # 创建Hello class >>> h = Hello() >>> h.hello() Hello, world. >>> print(type(Hello)) <class 'type'> >>> print(type(h)) <class '__main__.Hello'>
面向对象遵循的设计原则: SOLID
S(Single Responsibility Principle)单一职责原则,一个类只负责一项职责
好处:易于维护, 写出高内聚的代码;易于代码复用
O(Open Closed Principle) 开放封闭原则
对扩展开放
对修改关闭
易于维护, 保证代码安全性以及扩展性
L(Liskov Substitution Principle) 里氏替换原则,使用基类引用的地方必须能使用继承类的对象
好处
防止代码出现不可预知的错误
方便针对于基类的测试代码, 可以复用在子类上
I(Interface Segregation Principle)接口分离原则
如果一个类包含了过多的接口方法,而这些方法在使用的过程中并非"不可分割", 那么应当把他们进行分离
所谓接口, 在Python中, 可以简单的理解为"抽象方法"
好处
提高接口的重用价值
D(Dependency Inversion Principle)依赖倒置原则
高层模块不应该直接依赖低层模块
他们应该依赖抽象类或者接口
好处
利于代码维护