python面向对象—-类的定义与实例化

python是一种面向对象编程语言,自然也有类的概念。python中的类通过class 关键字定义,提供了面向对象的所有标准特性,例如允许一个类继承多个基类, 子类可以覆盖父类的方法,封装,继承,多态 面向对象的三大特性python一样不少。

1.类的定义

定义一个类使用class关键字

  1. class Stu:
  2. def __init__(self, name, age):
  3. self.name = name
  4. self.age = age
  5. def run(self):
  6. print("{name} is running".format(name=self.name))

关于这段代码有很多新的概念需要你了解

1.1 类的名字

上面所定义的类的名字叫Stu,你可以使用Stu的name属性来访问它

  1. print(Stu.__name__)

1.2 实例方法

在函数一章,你已经学习使用def来定义函数,在类里面,同样用def关键字来定义函数,但是我们管类里的函数叫方法,在这个示例中所有的方法都是实例方法,如不特殊说明,本教程中所说的方法均指实例方法。

1.3 初始化函数与实例属性

注意看这个方法

  1. def __init__(self, name, age):
  2. self.name = name
  3. self.age = age

单词init是初始化的意思,init是初始化函数,当实例被构造出来以后,使用init方法来初始化实例属性。
name 和 age都是实例的属性,属性就是数据。说了半天实例,到底谁是实例,在类的方法里,self就是实例,如果你有其他编程语言的经验,python中的self等同于其他编程语言里的this。

2. 实例化

类是一种封装技术,我们把数据和方法都封装到了类里,如果你想使用数据和方法,那么你需要用类创建出一个实例来,这个过程就叫做实例化

  1. class Stu:
  2. def __init__(self, name, age):
  3. self.name = name
  4. self.age = age
  5. def run(self):
  6. print("{name} is running".format(name=self.name))
  7. s = Stu('小明', 18)
  8. s.run()

代码执行结果

  1. <class '__main__.Stu'>
  2. 小明 is running

s 就是创建出来的实例,s是类Stu的一个实例。在定义Stu的时候,要求实例必须有name和age属性,因此,在创建示例时,我们必须提供这些属性,按照init函数的参数列表传入’小明’ 和 18。
s.run() 这行代码是在调用实例的run方法,不同于之前所学习的函数,类里的方法只能通过实例或者类来调用,调用的方式就如代码里演示的那样。

3. self是什么?

你或许已经注意到,类里定义的每个方法都有一个self参数,它并不是默认参数,但这个参数在方法被调用时是不需要传参的,这与之前所学过函数内容相违背。
self是方法的一个参数,在方法调用时,这个参数是默认传参的。

  1. class Stu:
  2. def __init__(self, name, age):
  3. print("在__init__方法中", id(self))
  4. self.name = name
  5. self.age = age
  6. def run(self):
  7. print("在run方法中", id(self))
  8. print("{name} is running".format(name=self.name))
  9. s = Stu('小明', 18)
  10. print("s的内存地址", id(s))
  11. s.run()

程序执行结果

  1. __init__方法中 4336221600
  2. s的内存地址 4336221600
  3. run方法中 4336221600
  4. 小明 is running

self的内存地址,和s的内存地址是一致的,这可以证明,self就是s。函数被定义以后,在任何地方都可以调用,没有调用的主体,而类的方法,则需要一个调用主体,哪个实例调用了类的方法,self就会绑定为哪个实例,self这个参数就是哪个实例。
分析上面的输出结果,在init方法被调用时,s这个对象就已经被创建好了,这可以证明它不是构造方法,真正的构造方法是new,在没有调用构造方法前,对象是不存在的。

4.对象属性的访问

对象属性的访问与修改,只能通过对象来进行

  1. class Stu:
  2. def __init__(self, name, age):
  3. self.name = name
  4. self.age = age
  5. def info(self):
  6. print("{name}今年{age}岁".format(name=self.name, age=self.age))
  7. def run(self):
  8. print("{name} is running".format(name=self.name))
  9. def print(self):
  10. print("ok")
  11. s = Stu("小刚", 18)
  12. s.info()
  13. print(s.name)
  14. s.age = 20
  15. s.info()

代码输出结果

  1. 小刚今年18
  2. 小刚
  3. 小刚今年20

5. 使用类组合数据和方法

一个名为 成绩单 的文件中保存了学生的考试成绩,内容如下

  1. 姓名 语文 数学 英语
  2. 小红 90 95 90
  3. 小刚 91 94 93

请编写程序读取该文件,使用合适的数据类型保存这些数据,输出每一个人的各个科目和分数,并计算每个学生的总的分数。

5.1 面向过程

  1. stus = []
  2. with open('成绩单', 'r', encoding='utf-8') as file:
  3. lines = file.readlines()
  4. for i in range(1, len(lines)):
  5. line = lines[i]
  6. arrs = line.split()
  7. stus.append(arrs)
  8. # 记住,每个列表里,第一个元素是姓名,第二个元素语文分数,后面的是数学分数,英语分数
  9. for stu in stus:
  10. msg = "{name}的各科成绩如下: 语文:{yw_score}, " \
  11. "数学:{sx_score}, 英语:{en_score}".format(name=stu[0],
  12. yw_score=stu[1],
  13. sx_score=stu[2],
  14. en_score = stu[3]
  15. )
  16. print(msg)
  17. msg = "{name}的总成绩是{socre}".format(name=stu[0],
  18. socre=int(stu[1])+int(stu[2])+int(stu[3]))
  19. print(msg)

5.2 面向对象

  1. class Stu:
  2. def __init__(self, name, yw, sx, en):
  3. self.name = name
  4. self.yw = yw
  5. self.sx = sx
  6. self.en = en
  7. def score_sum(self):
  8. return self.yw + self.sx + self.en
  9. def print_score(self):
  10. msg = "{name}的各科成绩如下: 语文:{yw_score}, " \
  11. "数学:{sx_score}, 英语:{en_score}".format(name=self.name,
  12. yw_score = self.yw,
  13. sx_score = self.sx,
  14. en_score = self.en
  15. )
  16. print(msg)
  17. msg = "{name}的总成绩是{score}".format(name=self.name, score=self.score_sum())
  18. print(msg)
  19. stus = []
  20. with open('成绩单', 'r', encoding='utf-8') as file:
  21. lines = file.readlines()
  22. for i in range(1, len(lines)):
  23. line = lines[i]
  24. arrs = line.split()
  25. s = Stu(arrs[0], int(arrs[1]), int(arrs[2]), int(arrs[3]))
  26. stus.append(s)
  27. for stu in stus:
  28. stu.print_score()

如果仅仅从代码量上来看,使用类并没有减少代码,但是从代码的可阅读性上看,使用类组织数据和方法显然更具有优势,而且,类这种概念,更加符合我们人类的思维。
我们人喜欢把各种东西归类,爬行动物,恒温动物,冷血动物,猫科动物,犬科动物,这些都是类。
狮子也是一个类,是猫科动物的子类,具体到一个真实存在的狮子,我们可以认为这个真实存在的狮子是狮子这个类的一个实例,或者说狮子这个类的一个对象。
使用类组织数据和方法,扩展性更好,可以随时添加新的属性和方法,比如,我想输出学生最高的科目分数,那么,我只需要修改类就可以了

  1. class Stu:
  2. def __init__(self, name, yw, sx, en):
  3. self.name = name
  4. self.yw = yw
  5. self.sx = sx
  6. self.en = en
  7. def score_sum(self):
  8. return self.yw + self.sx + self.en
  9. def get_max_score(self):
  10. if self.yw > self.sx:
  11. if self.yw > self.en:
  12. return self.yw
  13. else:
  14. return self.en
  15. else:
  16. if self.sx > self.en:
  17. return self.sx
  18. else:
  19. return self.en
  20. def print_score(self):
  21. msg = "{name}的各科成绩如下: 语文:{yw_score}, " \
  22. "数学:{sx_score}, 英语:{en_score}".format(name=self.name,
  23. yw_score = self.yw,
  24. sx_score = self.sx,
  25. en_score = self.en
  26. )
  27. print(msg)
  28. msg = "{name}的总成绩是{score}".format(name=self.name, score=self.score_sum())
  29. print(msg)
  30. msg = "最高分数是{max_score}".format(max_score = self.get_max_score())
  31. print(msg)

在程序里,调用对象print_score方法的地方不用做任何的修改。反观5.1 的写法,你不得不在for循环里做大量的修改,这样的代码可维护性是很差的。

6. 实例方法,类方法,静态方法, 类属性

类里的方法有3种,在第一小节中所定义的类中,方法都是实例方法。这3种方法有各自的定义方式和权限

名称 定义方法 权限 调用方法
实例方法 第一个参数必须是示例,一般命名为self 可以访问实例的属性和方法,也可以访问类的实例和方法 一般通过示例调用,类也可以调用
类方法 使用装饰器@classmethod修饰,第一个参数必须是当前的类对象,一般命名为cls 可以访问类的实例和方法 类实例和类都可以调用
静态方法 使用装饰器@staticmethod修饰,参数随意,没有self和cls 不可以访问类和实例的属性和方法 实例对象和类对象都可以调用

下面修改类Stu的定义来向你展示这3种方法的区别

  1. class Stu:
  2. school = '湘北高中' # 类属性
  3. def __init__(self, name, age):
  4. self.name = name
  5. self.age = age
  6. def play_basketball(self):
  7. print("{name} 正在打篮球".format(name=self.name))
  8. @classmethod
  9. def sport(cls):
  10. print("{school}的同学都喜欢篮球".format(school=cls.school))
  11. @staticmethod
  12. def clean():
  13. print("打扫教室卫生")
  14. stu = Stu("樱木花道", 17)
  15. stu.play_basketball() # 通过实例调用实例方法
  16. Stu.play_basketball(stu) # 通过类调用实例方法
  17. Stu.sport() # 通过类调用类方法
  18. stu.sport() # 通过示例调用类方法
  19. Stu.clean() # 通过类调用静态方法
  20. stu.clean() # 通过实例调用静态方法
  21. print(stu.school) # 通过实例访问类属性
  22. stu.school = '山王工业' # 只是增加了一个实例属性,类属性不会被修改
  23. print(Stu.school) # 通过类访问类属性

代码执行结果

  1. 樱木花道 正在打篮球
  2. 樱木花道 正在打篮球
  3. 湘北高中的同学都喜欢篮球
  4. 湘北高中的同学都喜欢篮球
  5. 打扫教室卫生
  6. 打扫教室卫生
  7. 湘北高中
  8. 湘北高中

理解这3种方法的核心在于理解他们的权限。
在面向对象的设计理念中,方法必定属于某个类,如果这个方法不属于某个类,那么它就是函数了,就回归到了面向过程编程。方法属于某个类,但这个方法可能不会访问实例和类的任何属性或其他方法,对于这种方法,我们就应该把它设计成静态方法。
还有一种可能,一个方法会访问到类的属性,就像本示例中的sport方法,整个湘北高中的学生都喜欢篮球,这个方法就不是某个学生所特有的,而是整个类所拥有的一个方法,那么就需要把它设计成类方法。school是类属性,既然类拥有这个属性,那么类实例化出来的对象也自然拥有这个属性,通过类和示例都可以访问到这个属性,但是要注意,这个属性是类的,因此不能通过实例对它进行修改。

python面向对象—-类的封装

python的面向对象, 并没有严格意义上的私有属性和方法, 私有只是一种约定, 隐藏实现的细节,只对外公开我们想让他们使用的属性和方法,这就叫做封装,封装的目的在于保护类内部数据结构的完整性, 因为使用类的用户无法直接看到类中的数据结构,只能使用类允许公开的数据,很好地避免了外部对内部数据的影响,提高了程序的可维护性。用户只能通过暴露出来的方法访问数据时,你可以在这些方法中加入一些控制逻辑,以实现一些特殊的控制

1 私有属性和方法

面向对象的内容,在学习时要侧重其理念,但理念这东西需要实践加持,否则,很难理解。
假设,你和你的同事一起做项目,现在,需要你写一个类让同事使用,这个类,你定义了5个属性,10个方法,但是呢,你的同事其实只会用到其中一个方法,剩下那9个,都是用到的那个方法在执行过程中调用的方法。
那么问题来了,你明确告诉他,你就调用那个A方法就好了,其他方法,你不要使用,不然,可能会影响结果。如果你的同事是个很安分守己的人,他听从了你的建议,老老实实的调用A方法,其他方法,他一概不动,这样就是安全的。
可是过了几天,来了新同事,他偏偏不听话,非得调用剩余的9个方法,它觉得,自己调用那9个方法,可以更好的实现功能,结果呢,出了大问题了,那9个方法,他用的不好,导致程序出错了。
我们写了一个类,有些属性,有些方法,我们不希望被其他人使用,因为那样很容易就产生错误,那么这时,我们就需隐藏实现的细节,只对外公开我们想让他们使用的属性和方法,这就叫做封装。就好比把一些东西用一个盒子封装起来,只留一个口,内部让你看不见。
如何才能做到这一点呢,在python里,如果属性和方法前面是双下划线,那么这个属性和方法就变成了私有的属性。

  1. class Animal:
  2. def __init__(self, name, age):
  3. self.__name = name
  4. self.__age = age
  5. def __run(self):
  6. print("run")
  7. a = Animal('猫', 2)
  8. a.__run()
  9. print(a.__name, a.__age)

__run()方法是以双下划线开头的,这样的方法,类的对象无法使用,双下划线开头的属性也同样无法访问,通过这种方法,就可以将不希望外部访问的属性和方法隐藏起来。
属性隐藏起来,并非不让外部使用,而是在使用时收到控制,比如年龄,如果年龄属性定义成age,那么就会出现这样的情况

  1. a.age = 10000

你见过哪个动物的年龄有10000岁呢,这显然是有问题的,但age属性暴露出来了,别人在使用时就可能会犯这样的错误,所以,为了防止这样的错误发生,就可以将age属性设置成私有的,然后提供一个set_age方法

  1. class Animal:
  2. def __init__(self, name, age):
  3. self.__name = name
  4. self.__age = age
  5. def set_age(self, age):
  6. if age > 100 or age < 1:
  7. raise Exception("年龄范围错误")
  8. self.__age = age
  9. def get_age(self):
  10. return self.__age
  11. def __run(self):
  12. print("run")
  13. a = Animal('猫', 2)
  14. a.set_age(3)
  15. print(a.get_age())
  16. a.set_age(101)

2. 类属性

类可以有自己属性,这个属性不同于实例的属性,类属性属于类,但是可以被实例访问

  1. class People(object):
  2. country = '中国'
  3. print(id(People.country), People.country) # 通过类去访问
  4. p = People()
  5. # 实例对象p并没有country属性,但会从类属性里找到同名的属性
  6. print(id(p.country), p.country)
  7. p.country = "美国" # 创建了country实例属性, 而不是修改了类的country属性
  8. print(id(p.country), p.country)
  9. print(id(People.country), People.country)

3. classmethod

在这之前,所有的示例代码中,类中的方法都是普通方法,第一个参数是self,这种方法只能被实例调用,如果被classmethod装饰,这个方法就是类方法,可以被类和实例调用

  1. class People(object):
  2. country = '中国'
  3. @classmethod
  4. def sing_the_national_anthem(cls):
  5. print('唱{country}国歌'.format(country=cls.country))
  6. People.sing_the_national_anthem()
  7. p = People()
  8. p.sing_the_national_anthem()

如果一个方法不访问示例的属性,但会访问类的属性和方法时,就可以设置成类方法。

4. staticmethod

如果一个方法,既不会访问示例属性或方法,也不会访问类的属性或方法,方法里的都是有关逻辑性的代码,那么这种代码就可以设置成静态方法,你可能会好奇,既然这个方法跟类和实例都没啥交互,那要这个方法做什么呢?
回到我们最初描述类的定义上,类提供了一种组合数据和功能的方法,我们定义一个类,是希望这个类能够完成特定的功能,但这里的功能不一定非得访问什么属性啊,它只需要完成我们期望的事情就好了,将这个方法定义在这个类里,是因为它的逻辑和这个类在逻辑功能上有关系,所以,放在一起,便于管理

  1. class People(object):
  2. country = '中国'
  3. @staticmethod
  4. def sing_the_national_anthem():
  5. print('唱国歌')
  6. People.sing_the_national_anthem()
  7. p = People()
  8. p.sing_the_national_anthem()

5. 三种类方法对比

名称 定义方法 权限 调用方法
实例方法 第一个参数必须是示例,一般命名为self 可以访问实例的属性和方法,也可以访问类的实例和方法 一般通过示例调用,类也可以调用
类方法 使用装饰器@classmethod修饰,第一个参数必须是当前的类对象,一般命名为cls 可以访问类的实例和方法 类实例和类都可以调用
静态方法 使用装饰器@staticmethod修饰,参数随意,没有self和cls 不可以访问类和实例的属性和方法 实例对象和类对象都可以调用

python面向对象—-类的继承

继承是python面向对象的三大特性之一,是一种创建新类的方式,python中的继承,可以继承一个或者继承多个父类,新建的类被称之为派生类或者子类,被继承的类是父类,可以称之为基类,超类,继承是实现代码重用的重要方式。

1. 单继承

先看单继承的例子

  1. class Car(object):
  2. def __init__(self, speed, brand):
  3. self.speed = speed
  4. self.brand = brand
  5. def run(self):
  6. print("{brand}在行驶".format(brand=self.brand))
  7. # 燃油车
  8. class Gasolinecar(Car):
  9. def __init__(self, speed, brand, price):
  10. super().__init__(speed, brand)
  11. self.price = price
  12. class Audi(Gasolinecar):
  13. pass
  14. honda = Gasolinecar(130, '本田', 13000)
  15. honda.run()
  16. audi_car = Audi(100, '奥迪', 10000)
  17. audi_car.run()
  18. print(issubclass(Audi, Gasolinecar)) # 判断Audi是Gasolinecar的子类
  19. print(issubclass(Gasolinecar, Car))

代码执行结果

  1. 本田在行驶
  2. 奥迪在行驶
  3. True
  4. True

2. 继承发生了什么

继承,意味着子类将“拥有”父类的方法和属性,同时可以新增子类的属性和方法。在Gasolinecar类里,我没有写run方法,但是Gasolinecar的父类定义了run方法,因此,Gasolinecar也有这个方法,因此这个类的对象honda可以使用run方法。
Audi类没有定义任何方法,但是它继承了Gasolinecar,因此,Gasolinecar有的属性和方法,它都拥有,这里就包括了init方法。
super()可以用来调用父类的方法,Gasolinecar多传了一个price属性,其父类的init方法里有两个参数,因此,可以先调用父类的init方法初始化speed, brand,然后在初始化price。

3. 多继承

  1. class Person():
  2. def person_walk(self):
  3. print("走路")
  4. class Wolf():
  5. def wolf_run(self):
  6. print("奔跑")
  7. class WolfMan(Person, Wolf):
  8. def __init__(self, state):
  9. # 1表示狼形态,2表示人形态
  10. self.state = state
  11. def change(self):
  12. if self.state == 1:
  13. self.state = 2
  14. else:
  15. self.state = 1
  16. def run(self):
  17. if self.state == 1:
  18. self.wolf_run()
  19. else:
  20. self.person_walk()
  21. wolf_man = WolfMan(1)
  22. wolf_man.run()
  23. wolf_man.change()
  24. wolf_man.run()

4. 继承的优势

有了继承,创建一个新的类就非常的方便,因为只需要继承以前的父类就可以了,继承之后,子类拥有了父类的属性和方法,这样不仅仅是节省了代码,更重要的是,配合多态,让类的行为更加丰富

python面向对象—-类的多态

python面向对象的多态依赖于继承, 因为继承,使得子类拥有了父类的方法, 子类的方法与父类方法重名时是重写, 同一类事物,有多重形态, 这就是面向对象概念里的多态,多态使得不同的子类对象调用相同的 类方法,产生不同的执行结果,可以增加代码的外部调用灵活度。

1. 多态

1.1 重写

多态这个概念依赖于继承,因为继承,使得子类拥有了父类的方法,这里就产生了一个问题,如果子类有一个方法和父类的方法同名,那么子类在调用这个方法时,究竟是调用子类自己的方法,还是父类的方法?

  1. class Base():
  2. def print(self):
  3. print("base")
  4. class A(Base):
  5. def print(self):
  6. print("A")
  7. a = A()
  8. a.print()

父类和子类都有print方法,那么子类A的对象a调用print方法时,调用的是谁的print方法呢?
答案是子类的print方法,如果A类没有定义print方法,那么a.print()调用的是父类的print方法,但是A类定义了print方法,这种情况称之为重写,A类重写了父类的print方法。

1.2 继承的搜索

强调继承时,子类“拥有”父类的方法和属性,特意加了双引号,因为,这种拥有不是真实意义上的拥有。

  1. class Base():
  2. def print(self):
  3. print("base")
  4. class A(Base):
  5. pass
  6. print(id(Base.print))
  7. print(id(A.print))

Base.print 和 A.print 的内存地址是相同的,这说明他们是同一个方法。执行A.print时,python会寻找print方法,它会先从A类的定义中寻找,没有找到,然后去A的父类里寻找print方法,如果还是找不到,就继续向上寻找。
这便是子类拥有父类属性和方法的真相

1.3 多态的表现形式

同一类事物,有多重形态

  1. class Animal:
  2. def run(self):
  3. raise NotImplementedError
  4. class People(Animal):
  5. def run(self):
  6. print("人在行走")
  7. class Pig(Animal):
  8. def run(self):
  9. print("猪在跑")
  10. p1 = People()
  11. p1.run()
  12. p2 = Pig()
  13. p2.run()

People和 Pig 都继承了Animal,都是动物,是同一类事物,他们都有run方法,但是最终的运行结果却不一样,这就是多态,同一类事物有多种形态。