Python 面向对象(一) 基础
Python 中一切皆对象
什么是面向对象?
面向对象就是将一些事物的共有特征抽象成类,从类来创建实例。
类class
可以理解为模版
比如人类,都具有身高、体重、年龄、性别、籍贯。。。等属性,但属性的值都不尽相同。
对象instance、object
对象是类抽象出来的一个实例。比如小鸣同学和小红同学,他们都是人类这个类抽象出来的一个人,但小鸣是男同学,小红是女同学,他们都各自的身高、体重。。。等等属性。
属性
属性是对象状态的抽象,用数据结构来描述。
操作(动作、方法)
操作是对象行为的抽象,用操作名和实现该操作名的方法来描述
面向对象三要素
1. 封装
组装:将数据和操作组装到一起
隐藏数据:对外只暴露一些接口,通过接口访问对象。(隐藏细节)
2. 继承:
代码复用:继承来的就不用自己写了
3. 多态:
接口重用,一个接口,多种形态。
举例:比如每个动物都有吃这个动作,如果让狗、猫、鸡去吃,那就得分别调用狗的吃方法、猫的吃方法、鸡的吃方法。虽然调用了3次都做了吃这个动作,但如果有100个动物呢?那就可以使用多态的特性了,我可以封装一个接口,接口的作用是接受传入的动物名去执行吃这个动作,这样就可以理解成只需要调用这个接口,分别传入狗、猫、鸡,就可以执行吃这个动作了。
如何定义类:
class ClassName: 语句块
1. 必须使用class关键字
2. 类名必须是大驼峰方式命名
3. 类定义完成后,就产生了一个类对象,绑定到了ClassName上
实例化:
instancename = ClassName()
构造函数:
实例创建时类的初始化函数,初始化对象的一些属性,这些属性会bound到对象上。
类变量:
类的变量是类的所有实例可以共享的一些变量,比如都是在学习Pyhon课程,那么这些课程这个变量就可以定义为类的变量而不用每次都单独定义一个同样的课程变量。
实例变量:
假如学生是一个实例,那么每个学生的名字、年龄、身高、体重等各自的特征就是实例本身的变量,教室地点、课程名这两个所有学生都共有的变量就可以定义成类变量。
举例:
class MyClass: '''a example class''' address = 'ShanXi' #类变量 course = 'Python' def __init__(self, name, age, length=170, width=110): #构造函数,初始化函数 self.name = name #实例变量 self.age = age self.length = length self.width = width def info(self): print(self.name, self.age, self.address, self.course, self.length, self.width) print('MyClass: ', MyClass.__dict__) s1 = MyClass('zhangsan', '18') #实例1 print(s1.__dict__) print(s1.name, s1.age, s1.address, s1.course, s1.length, s1.width) s2 = MyClass('lisi', '23', width=130) #实例2 print(s2.__dict__) print(s2.name, s2.age, s2.address, s2.course, s2.length, s2.width)
输出结果:
MyClass: {'__module__': '__main__', 'course': 'Python', '__dict__': <attribute '__dict__' of 'MyClass' objects>, '__doc__': 'a example class', 'info': <function MyClass.info at 0x100774ae8>, '__init__': <function MyClass.__init__ at 0x100774a60>, '__weakref__': <attribute '__weakref__' of 'MyClass' objects>, 'address': 'ShanXi'} {'width': 110, 'length': 170, 'age': '18', 'name': 'zhangsan'} zhangsan 18 ShanXi Python 170 110 {'width': 130, 'length': 170, 'age': '23', 'name': 'lisi'} lisi 23 ShanXi Python 170 130
在实例化一个类以后,都会自动调用一个__init__方法,方法的第一个参数只能是self,表示接受的参数是实例本身。
__init__方法可以有多个行参,可以使用位置参数、关键字参数。
__init__方法不能有返回值,也就是只能为None。
通过__dict__特殊方法,可以查看类或者实例各自的变量:
类的__dict__方法,保存的是所有实例共有的变量
实例的__dict__方法,保存的是实例自身的变量。
所以实例的变量有个查找顺序,只有在实例没有这个变量时才会去类的变量中查找,如果类中有就返回。
如果实例定义了一个与类变量同名的变量,那实例会新增这个变量,覆盖掉类的变量。
比如: s1 和 s2 两个实例本身并没有变量address、course,但类里面有,所以上面例子中仍然可以通过实例的address、course变量。
如果实例 s1 定义了与类变量同名的变量 address,那么s1 的这个私有变量就会覆盖掉类的变量,示例:
class MyClass: '''a example class''' address = 'ShanXi' course = 'Python' def __init__(self, name, age, length=170, width=110): self.name = name self.age = age self.length = length self.width = width s1 = MyClass('zhangsan', '18') print(s1.name, s1.age, s1.address, s1.course) s1.address = 'ShangHai' #实例变量赋值 print(s1.name, s1.age, s1.address, s1.course) s2 = MyClass('lisi', '23', width=130) print(s2.name, s2.age, s2.address, s2.course)
返回结果:
zhangsan 18 ShanXi Python zhangsan 18 ShangHai Python lisi 23 ShanXi Python
可以看到实例s1 的address 变量赋值为"ShangHai" 后,覆盖了类的值"ShanXi",但是并没有影响到实例s2 的address值(s1.__dict__)。
一般来说,类的变量名应该使用全大写来命名。
装饰一个类
装饰器函数是对普通函数的功能增强。装饰器函数也是一个函数,可以接收一个函数作为行参,在内部对函数功能做修饰后,返回函数结果。
需求:为一个类通过装饰,增加一些类属性
# 增加类变量 # def add_name(name, clz): # clz.NAME = name # 改进成装饰器 def add_name(name): def wrapper(clz): clz.NAME = name return clz return wrapper @add_name('Jack') class MyClass: AGE = 30 print(MyClass.NAME)
特殊方法 含义
__name__ 对象名
__class__ 对象的类型
__dict__ 对象的属性的字典
__qualname__ 类的限定名
类方法和静态方法
class Person: ADDRESS = 'BeiJIng' COURSE = 'Python' def __init__(self, name, age): self.name = name self.age = age def normal_method(): print('normal') def method(self): print('{} is method'.format(self)) @classmethod def class_method(cls): print('class = {0} ({0.COURSE})'.format(cls)) # print('instance attribute name = {}'.format(cls.name)) cls.HEIGHT = 170 @staticmethod def static_method(): print('static method') print('分别测试类和实例是否可以访问以上4种方法') print('类访问~~~~~~~~~~~~') print(1, Person.normal_method()) #类访问普通方法,可以 # print(2, Person().normal_method()) #等同于实例访问普通方法,不可以 # print(3, Person.method()) #类访问实例方法,不可以 print(4, Person.class_method()) #类访问类方法,可以 print(5, Person.static_method()) #类访问静态方法,可以 print('实例访问~~~~~~~~~~~~') s1 = Person('jack', 20) #实例化 # print(1, s1.normal_method()) #实例访问普通方法,不可以 print(2, s1.method()) #实例访问实例方法,可以 print(3, s1.class_method()) #实例访问类方法,可以,但是只能访问类变量 print(4, s1.static_method()) #实例访问静态方法,可以
normal_method普通方法 不接收参数,类可以调用,但实例不可以调用,不可以访问类变量和实例变量
实例方法 实例方法,指类中可以接收self参数的函数。既可以访问类变量,也可以访问实例变量
staticmethod 静态方法,就是类里面的普通函数,但实例可以调用。 静态方法装饰的函数不可以访问类变量和实例变量
classmethod 类方法,只能访问类变量,不能访问实例的变量
总结:
类方法几乎可以调用所有内部定义的方法,但是调用实例方法时会报错,原因是第一参数必须是类的实例。
实例也几乎可以调用所有的方法,但是不可以调用普通方法,因为实例调用时会隐式传入实例,但普通方法根本不接收参数。
类可以调用:普通方法,类方法,静态方法
实例可以调用:实例方法,类方法,静态方法
私有(Private)属性
举例:
class Person: def __init__(self, name, age=18): self.name = name self.age = age def growup(self, i=1): if i > 0 and i < 150: print(self.age) self.age += i print(self.age) p1 = Person('tom') p1.growup(10) print(p1.__dict__) p1.age = 150 p1.growup(20) print(p1.__dict__)
上面的例子中,本来是想通过方法控制属性,但是由于属性在外部可以访问,或者说可见,就可以直接绕过方法,直接修改这个属性。
Python提供了私有属性可以解决这个问题
私有属性
使用双下划线开头的属性名,就是私有属性
class Person: def __init__(self, name, age=18): self.name = name self.__age = age def growup(self, i=1): if i > 0 and i < 150: print(self.__age) self.__age += i print(self.__age) p1 = Person('tom') p1.growup(10)
print(p1.__dict__) # print(p1.__age) #外部访问不到
输出结果会报错:
18 28 {'_Person__age': 28, 'name': 'tom'} Traceback (most recent call last): File "/Users/zhangsan/Desktop/python/1106面向对象/Private_test1.py", line 15, in <module> print(p1.__age) #外部访问不到 AttributeError: 'Person' object has no attribute '__age'
通过上面这个例子可以看出,外部已经无法访问__age变量了,在__dict__中也可以看到age被改名成了_Person__age。
可以在类中通过方法来访问这个私有变量__age,也可以实例化之后调用这个接口访问。
class Person: def __init__(self, name, age=18): self.name = name self.__age = age def growup(self, i=1): if i > 0 and i < 150: self.__age += i def getage(self): return self.__age p1 = Person('tom') p1.growup(10) print(p1.getage())
输出结果:
28
在为实例动态添加一个__age变量时,不会覆盖掉原来的__age。
class Person: def __init__(self, name, age=18): self.name = name self.__age = age def growup(self, i=1): if i > 0 and i < 150: # print(self.__age) self.__age += i # print(self.__age) def getage(self): return self.__age p1 = Person('tom') p1.growup(10) # print(p1.__age) #访问不到 print(p1.getage()) print(p1.__dict__) p1.__age = 150 print(p1.__age) # 150,没有覆盖__age变量 print(p1.getage()) print(p1.__dict__)
输出结果:
28 {'name': 'tom', '_Person__age': 28} #没有__age变量,但有一个很奇怪的变量,稍后介绍 150 28 {'name': 'tom', '__age': 150, '_Person__age': 28}
原因是变量有查找顺序,也就是先找实例变量-再找类变量,会发现实例和类都没有__age这个变量,所以创建了一个__age变量,这个变量属于实例变量,所以等同于重新为一个同名变量赋值而已。
实例__dict__属性,保存的是关于实例自身的一些变量
类的__dict__属性,保存是所有实例共享的一些变量。
类定义的时候,如果声明一个实例变量的时候,使用双下划线,Python解释器会将其改名,转换名称为 _类名__变量名 的名称,所以用原来的名字访问不到了。但是如果外部知道了修改之后的名字,在外部也可以直接访问或修改。可见python变量并没有绝对的保护起来。
class Person: def __init__(self, name, age=18): self.name = name self.__age = age def growup(self, i=1): if i > 0 and i < 150: # print(self.__age) self.__age += i # print(self.__age) def getage(self): return self.__age p1 = Person('tom') p1.growup(10) # print(p1.__age) #访问不到 print(p1.getage()) print(p1.__dict__) p1.__age = 150 print(p1.__age) #没有覆盖__age变量 print(p1.getage()) print(p1.__dict__) #直接修改私有变量 p1._Person__age = 80 print(p1.getage()) #80 可以直接修改 print(p1.__dict__)
输出结果:
28 {'_Person__age': 28, 'name': 'tom'} 150 28 {'_Person__age': 28, '__age': 150, 'name': 'tom'} 80 {'_Person__age': 80, '__age': 150, 'name': 'tom'}
保护变量
在变量名前使用一个下划线,称为保护变量。
class Person: def __init__(self, name, age=18): self.name = name self._age = age p1 = Person('jack') print(p1._age) #p1._age = 22 #不要直接使用 print(p1.__dict__)
输出结果:
18 {'_age': 18, 'name': 'jack'}
这个_age属性根本就没有改变名称,和普通的属性一样,解释器不做任何特殊处理。
这只是开发者共同的约定,看见这种变量,就如同私有变量,不要直接使用。
私有方法
参照私有变量、保护变量,使用单下划线、双下划线命名方法。
class Person: def __init__(self, name, age=18): self.name = name self._age = age def _getname(self): return self.name def __getage(self): return self._age p1 = Person('jack') print(p1._getname()) # print(p1.__getage()) #无法访问此属性,被改名为'_Person__getage' print(p1._age) print(p1.__dict__) print(p1.__class__.__dict__) # print(p1._Person__getage())
补丁
想象这样一个场景:
我们公司内部维护有一套系统,最近有发现这套系统有个bug,但临时又不想立刻修复并推到线上,而是想等过两天版本大升级再统一将修复后的程序合并发布。
这样的情况下,就可以使用猴子补丁(monkey patch),在运行时对属性进行动态修改以修复bug。
1. 程序运行文件 test1.py
将调用到了bug模块的方法替换成补丁文件中的方法
from test2 import Person from test3 import get_score def monkeypatch4Person(): Person.get_score = get_score monkeypatch4Person() if __name__ == "__main__": print(Person().get_score())
2. 被调用的模块 test2.py
模拟从数据库中读取数据
class Person: def get_score(self): # connect to mysql ret = {'English':78, 'Chinese':86, 'History':82} return ret
3. 补丁文件 test3.py
修改数据,模拟修复
def get_score(self): return dict(name=self.__class__.__name__, English=88, Chinese=90, History=85)
继承
py2经典类深度优先 新式类广度优先
py3中都是新式类,广度优先
封装 将数据和操作(即属性和方法)组合起来,对外隐藏细节,只暴露接口供调用
继承 代码重用
多态 一个接口多种形态,接口重用
子类,也叫派生类,继承类
父类,也叫基类
多态
多态的前提:继承,覆盖
多继承
装饰器装饰类添加功能:
def printable(cls): def prt(self): return self.content cls.print = prt # cls.print = lambda self:self.content return cls @printable class Printabel: def __init__(self, content): self.content = content print(Printabel.__dict__) a = Printabel('abc') print(a.print())
输出结果:
{'__module__': '__main__', '__dict__': <attribute '__dict__' of 'Printabel' objects>, '__doc__': None, '__init__': <function Printabel.__init__ at 0x000001792E6B2BF8>, 'print': <function printable.<locals>.prt at 0x000001792E6B2840>, '__weakref__': <attribute '__weakref__' of 'Printabel' objects>} abc
Mixin
利用多继承,将其它类混入进入,本质上是修改MRO顺序。不要在Mixin类中添加__init__方法,以免覆盖初始化
class PrintableMixin: def prt(self): print('Mixin: {}'.format(self.content)) class Document: def __init__(self,content): self.content = content def prt(self): print(self.content) class Word(Document): def prt(self): print('Word prt: {}'.format(self.content)) class Pdf(Document): pass class PrintablePdf(PrintableMixin,Pdf): #混入的类尽量靠前,覆盖 pass print(PrintablePdf.mro()) word = PrintablePdf('python') word.prt()
输出结果:
[<class '__main__.PrintablePdf'>, <class '__main__.PrintableMixin'>, <class '__main__.Pdf'>, <class '__main__.Document'>, <class 'object'>] Mixin: python