Python学习之旅—面向对象进阶知识:类的命名空间,类的组合与继承

前言

  上篇博客笔者带领大家初步梳理了Python面向对象的基础知识,本篇博客将专注于解决三个知识点:类的命名空间,类的组合以及面向对象的三大特性之一继承,一起跟随笔者老看看今天的内容吧。



1.类的命名空间

   在上一篇博客中,我们提到过对象可以动态添加属性,一起来回忆下昨天的知识点,看如下的代码:

class A:
    pass

a = A()
a.name = ‘alex‘
print(a.name)

这里我们手动为a对象添加了一个属性name,然后直接打印可以得到a对象的名称。通过这个例子,我们可以引出类的命名空间。在Python中,创建一个类就会创建一个类的名称空间,用来存储类中定义的所有名字,这些名字称为类的属性。通常类中的属性有两种:静态属性和动态属性。如下:

  静态属性就是直接在类中定义的变量,例如我们上一篇博客中的role = ‘person‘,这里role就是一个静态属性,我们也称为类变量,该变量可直接通过类名调用。 
  动态属性就是定义在类中的方法,例如我们前面定义的attack方法,该方法表示所有的Person类都具有的动作。

  其中类的静态属性是共享给所有的对象的,而类的动态属性是绑定给所有对象的。我们还是来看看下面的例子,然后再来理解这句话:

class Person:
    role = ‘person‘

    def __init__(self, name, sex, aggressive=200):
        self.name = name
        self.sex = sex
        self.aggr = aggressive
        self.blood = 2000

    def attack(self, dog):
        print(‘%s attack %s‘ % (self.name, dog.name))
        dog.blood -= self.aggr

alex = Person(‘alex‘, ‘male‘, 250)
print(alex.role)  # 打印 person
print(id(alex.role))  # 打印18076480

carson = Person(‘carson‘, ‘male‘, 800)
print(carson.role)  # 打印 person
print(id(alex.role))  # 打印 18076480

通过上面的例子我们可知,静态属性可以被alex和carson两个对象共同使用,因为它们的内存地址值是一样的。但同时这又有一个问题,如果这个静态属性是一个计算器或者其他类型的共享变量,那么很有可能会导致线程安全问题,关于这点我们后面会讨论。在这里我们必须要明白所有对象共享类的静态属性。

而类的动态属性是绑定到所有对象的,即在每个对象的内存空间中都会存在一份类的动态属性的地址。其实也很好理解,因为不同对象调用同一个方法传入的参数不同,肯定会执行不同的结果,因此类的动态属性,即类里面定义的方法肯定不会是共享的,而是绑定到不同的对象上。

因此我们可以作如下的总结:

    对于类的静态属性,如果使用类名.属性的方式调用,那么调用的就是类中的属性;如果使用对象.属性的方式调用,Python会先从对象自己的内存空间中寻找是否存在该变量,如果存在,则使用自己的,如果没有,就是用类中定义的静态变量。

    而对于类的动态属性(也即类中定义的方法),如果这个方法本身就存在于类中,那么在对象的内存空间中是不会再存储一份的;但是该方法在类中的地址是会存储一份到对象的内存空间中,以方便对象通过该地址去类中寻找需要调用的方法。

   我们再来看如下的代码:

class A:
    country = "中国"

    def show_name(self):
        print(self.name)

a = A()
a.name = ‘alex‘
a.show_name()  # 打印alex
a.show_name = ‘egon‘
print(a.show_name)  # 打印egon

   同样,按照我们之前的分析,a.name=‘alex‘表示我们为a对象动态添加了一个属性name,并赋予了值alex。调用a.show_name()会打印出alex,因此此时对象a已经拥有一个name属性。紧接着,我们定义了一个a.show_name = ‘egon‘,注意这里依然表示为a对象动态添加一个属性,只不过这个属性名称和类中的方法名称show_name一样,因此打印a.show_name,会直接打印出egon,原因是show_name是对象a的一个属性,这里大家千万不要搞混了。接下来,我们来看看如果在上述代码的最后再调用print(a.show_name())会发生什么?

class A:
    country = "中国"

    def show_name(self):
        print(self.name)

a = A()
a.name = ‘alex‘

a.show_name()
a.show_name = ‘egon‘
print(a.show_name)
print(a.show_name()) # 会报错:TypeError: ‘str‘ object is not callable

我们可以看到当我们调用和对象属性同名的方法时报错。这是因为找名字和方法都会先从自己的内存空间中找,而名字在面向对象中只能代表一个东西,要么是方法名,要么是属性名,当对象找到了属性名,即使再调用方法,也会报错,因为此时我们已经将该名称看作是一个字符串,因此会报如上的错误。如果还不明白,我们再来看下面的一个小例子:

def a():
    print(‘aaa‘)
a = 20
a() # 报错:TypeError: ‘int‘ object is not callable

这里的报错其实和上面是同样的道理,a = 20,此时a已经是一个变量,即使我们在下面继续调用上面定义好的a函数,python依然会认为a是一个变量,而一个整型变量是不能被调用的,所以会报错:整型对象不能被调用。

     最后我们来通过一个简单的例子来熟悉下类的命名空间,一起看看如下的代码:

class A:
    country = "中国"

    def show_name(self):
        print(self.name)

a = A()
b = A()

print(A.country)
print(a.country)
print(b.country)

a.country = ‘英国‘
print(A.country)
print(a.country)  # 打印英国
print(b.country)

  除了倒数第二个print语句打印的是英国外,其余打印的都是中国。在上面的程序中,我们发现使用对象a调用了静态属性country,并赋值为英国,但是这并没有改变静态变量a的值,因为我们打印b.country看到的依然是中国。



2.组合

  再梳理完类的命名空间后,我们再来看下一个知识点:类的组合。类的组合主要用来解决代码的重复问题,从而降低代码的冗余,组合和继承的概念比较类似,关于组合我们会在下一个知识点讨论。组合表示的是包含的意思,是一种什么有什么的关系。我们一起来看看下面的2个例子。

  现在我们有这样一个需求:我们想计算一个圆环的面积和周长。怎么做呢?圆环是由两个圆组成的,圆环的面积是外面圆的面积减去内部圆的面积。圆环的周长是内部圆的周长加上外部圆的周长。

  此时,我们首先实现一个圆形类,计算一个圆的周长和面积。按照上面圆环面积和周长的计算方法,我们只需要在"环形类"中组合圆形的实例作为环形类的属性即可,说明白点,就是我们让圆形对象作为环形类的一个属性即可。一起来看看下面的例子就明白了:

from math import pi

class Circle:
    ‘‘‘
    定义了一个圆形类;
    提供计算面积(area)和周长(perimeter)的方法
    ‘‘‘
    def __init__(self,radius):
        self.radius = radius

    def area(self):
         return pi * self.radius * self.radius

    def perimeter(self):
        return 2 * pi *self.radius

circle =  Circle(10) #实例化一个圆
area1 = circle.area() #计算圆面积
per1 = circle.perimeter() #计算圆周长
print(area1,per1) #打印圆面积和周长

class Ring:
    ‘‘‘
    定义了一个圆环类
    提供圆环的面积和周长的方法
    ‘‘‘
    def __init__(self,radius_outside,radius_inside):
        self.outsid_circle = Circle(radius_outside) # 大圆对象Circle(radius_outside)为圆环类的一个属性
        self.inside_circle = Circle(radius_inside)  # 小圆对象Circle(radius_inside)为圆环类的一个属性

    def area(self): # 直接调用大圆对象的面积-小圆的面积即可得到圆环的面积
        return self.outsid_circle.area() - self.inside_circle.area()

    def perimeter(self):  # 直接调用大圆的周长+小圆的周长即可得到圆环的周长
        return  self.outsid_circle.perimeter() + self.inside_circle.perimeter()

ring = Ring(10,5) #实例化一个环形
print(ring.perimeter()) #计算环形的周长
print(ring.area()) #计算环形的面积

通过上面的例子可知,我们使用类的组合概念计算出了圆环的面积和周长,这里包含的的关系是圆环中有大圆和小圆,因此我们考虑使用圆的周长和面积来计算圆环的面积和周长。一句话总结:一个类的对象作为另一个类的属性,这就是组合。为了加深对类的组合关系的理解,我们再来看下面的例子。

     现在有这样的需求,我想实现一个选课系统,具体需求笔者会单独开通一篇博客进行说明。通过分析,我们知道,选课系统涉及到4个角色,分别为学生,讲师,班级和管理员。在做关联时,我们需要为学生关联课程。对于学生类而言,课程仅仅是它的一个属性;但是对于课程而言,它又是一个单独存在的类。既然课程是学生类的一个属性,换句话说即学生类里面有课程,因此这里我们就用到了组合的概念。一起来看看下面的代码:

class BirthDate:
    def __init__(self, year, month, day):
        self.year = year
        self.month = month
        self.day = day

class Couse:
    def __init__(self, name, price, period):
        self.name = name
        self.price = price
        self.period = period

class Student:
    def __init__(self, name, gender, birth, course):
        self.name = name
        self.gender = gender
        self.birth = birth  # birth对象作为学生类的一个属性存在,表示为学生的生日
        self.course = course  # 课程对象作为学生类的一个属性存在,表示学生上的什么课

    def teach(self):
        print(‘teaching‘)

stu = Student(‘carson‘, ‘male‘,
             BirthDate(‘1992‘, ‘11‘, ‘13‘),
             Couse(‘python‘, ‘19800‘, ‘4 months‘)
             )  # 从这里可以清楚地看到BirthDate(‘1992‘, ‘11‘, ‘13‘)和Couse(‘python‘, ‘19800‘, ‘4 months‘)两个对象作为类Student的属性来初始化一个实例对象stu.
print(stu.birth.year, stu.birth.month, stu.birth.day) # stu.birth相当于上面的生日对象BirthDate

print(stu.course.name, stu.course.price, stu.course.period)  # stu.course相当于上面的课程对象course

通过上面的例子可知,类的组合关系表示的是什么有什么的关系,即当类之间有显著不同,并且较小的类是较大的类所需要的组件时,用组合比较好。我们可以用一句话来总结下类的组合关系:只要涉及到谁里面有谁,而且可以抽象成类,那么就考虑使用组合。我们最后来看一个组合的例子,这里要举的一个例子是人狗大战的游戏,即人可以攻击狗,狗可以咬人,而且人可以有武器,武器可以抽象为一个类,因此这里可以使用组合的概念。来看如下的代码:

3.类的继承

   3.1 继承知识点入门

   继承也是为了解决代码的重用问题,从而达到减少代码冗余的目的,它和类的组合类似,但是不同点在于,继承是一种什么是什么的关系,例如老师类是人类,香蕉类是水果类。在Python3中,表示两个类之间的继承关系很简单,例如要表示B类要继承A类,我们直接使用如下的表达式即可:class  B(A)即可。这是在Python3中的新式类写法,关于新式类与经典类笔者将在下一篇博客中做一个系统的梳理和说明,本次将专注于类的继承初级知识。一起来看看如下的一个简单的例子:

class People:
    pass

class Animal:
    pass

class Student(People, Animal):  # People、Animal称为基类或父类,Student继承了People和Animal的所有属性
    pass

print(Student.__bases__)  # __bases__方法用来查看子类继承的所有父类,从做到右分别打印继承的父类
print(People.__bases__)
print(Animal.__bases__)上面三个print语句的打印结果如下:

(<class ‘__main__.People‘>, <class ‘__main__.Animal‘>)
(<class ‘object‘>,)
(<class ‘object‘

   上面的例子是一个多继承例子,Python中也有一个单继承的问题;来看如下的例子:

class Animal:
    pass

class Dog(Animal):
    pass

print(Dog.__bases__)  # 打印:(<class ‘__main__.Animal‘>,) 结果是一个元组

   由此可知,Python支持多继承和单继承,但对于多继承而言,并没有太大的意义,所以在实际开发中,我们推荐使用单继承。

  3.2 继承的重用性

  我们来看看继承是如何解决代码的冗余的。试想这样一个生活场景:猫和狗都具有吃饭,睡觉,喝水的功能,按照普通的类的定义方式,我们可以写出如下的代码:

class Dog:
    def __init__(self, name, food):
        self.name = name
        self.food = food

    def eat(self):
        print(‘%s eating %s‘ % (self.name, self.food))

    def drink(self):
        print(‘drinking‘)

    def sleep(self):
        print(‘sleeping‘)

class Cat:
    def __init__(self, name, food):
        self.name = name
        self.food = food

    def eat(self):
        print(‘%s eating %s‘ % (self.name, self.food))

    def drink(self):
        print(‘drinking‘)

    def sleep(self):
        print(‘sleeping‘)

dog = Dog(‘泰迪‘, ‘肉包子‘)
dog.eat()

cat = Cat(‘加菲猫‘, ‘鲫鱼‘)
cat.eat()

从上面代码不难看出,狗和猫都具有相同的功能,但是我们分别定义了两个狗和猫两个类,并实现了两遍相同的方法。这无疑增加了代码的冗余度,事实上按照继承的思想,我们可以将这些相同的功能抽象出来,并且抽象出一个共同类:动物类。代码如下:

class Animal:
    def __init__(self, name, food):
        self.name = name
        self.food = food

    def eat(self):
        print(‘%s eating %s‘ % (self.name, self.food))

    def drink(self):
        print(‘drinking‘)

    def sleep(self):
        print(‘sleeping‘)

class Dog(Animal):
    def __init__(self, name, food):
        super(Dog, self).__init__(name, food)

    def say(self):
        print("汪汪汪")

class Cat(Animal):
    def __init__(self, name, food):
        super(Cat, self).__init__(name, food)

    def say(self):
        print(‘喵喵喵‘)

dog = Dog(‘泰迪‘, ‘肉包子‘)
dog.eat()

cat = Cat(‘加菲猫‘, ‘鲫鱼‘)
cat.eat()

  可以看到再使用完继承的概念后,代码的冗余度和可读性都增强了。由上面的代码可知,子类会继承父类所有的方法和属性。同时我们发现在子类中,我们使用了super关键字来初始化对象:super(Cat, self).__init__(name, food)。这里我们手动地调用了父类中的init方法。

  很多同学对super关键字不熟悉,这里笔者来为大家做个小总结:

  1. super里面必须传入两个参数,第一个参数代表本类,第二个参数代表本类的对象。如果直接在子类里面使用super关键字调用父类方法,super关键字可以不用传递参数,即我想调用父类的init方法来进行初始化,可以写成这样:super(Cat, self).__init__(name, food)。

  2.什么时候使用super?如果子类和父类有同名的方法,此时还需要调用父类的方法,那么就应该使用super关键字。例如在上面的代码中,子类和父类都有init方法,此时我还想调用父类的init方法来初始化一个子类对象,所以我们用到了super关键字。

  3.在使用super关键字时,如果是在类外面想调用父类的方法,那我们必须要传递两个参数,第一个参数是子类名,第二个参数是子类对象;如果在类里面调用父类的方法,传入参数时,第一个参数必须是子类名,第二个参数为self,代表的是子类的对象。

  4.在实际开发中,我们使用的是在类里面使用super关键字来调用父类的方法,这样就做到了我调用子类的一个方法,又做到调用了父类的方法,一举两得。

3.2 派生属性和派生方法

     在前面我们说过,子类继承父类时,会继承父类所有的属性和方法。但是子类有时会有一些独有的属性和方法是父类所不具备的,我们还是通过实际的案例来说明该知识点,代码如下:

class Animal:
    def __init__(self, name, blood, aggr):
        self.name = name
        self.blood = blood
        self.aggr = aggr

class Person(Animal):
    def __init__(self, name, blood, aggr, money):
        super(Person, self).__init__(name, blood, aggr)
        self.money = money

    def attack(self, dog):
        dog.blood -= self.aggr

class Dog(Animal):
    def __init__(self, name, blood, aggr, breed):
        super(Dog, self).__init__(name, blood, aggr)
        self.breed = breed  # 派生属性 :在父类属性的基础上,之类特有的属性

    def bite(self, person):  # 派生方法:子类独有的方法
        person.blood -= self.aggr

dog = Dog("泰迪", 1000, 500, 1000000)
alex = Person("Alex", 2000, 50, "金毛")

dog.bite(alex)
print(dog.blood) # 1000
print(dog.breed)  # 1000000

alex.attack(dog)
print(alex.money)  # 金毛

还是人狗大战的例子,人和狗都是动物类,很自然,两者都继承于动物类,但是两者都有一些独有的属性和方法。例如对于人来说,人具有钱money这个属性,人的独有方法是攻击方法,它可以攻击任何对象;对于狗来说,它的独有属性是品种breed,它的独有方法是bite方法—咬人。像money,breed这些是子类独有的属性,我们称之为类的派生属性;而像attack(),bite()方法称之为类的派生方法。



结语:

     本篇博客主要专注于解决面向对象的进阶知识:类的命名空间,类的组合与继承。下一篇笔者将代理大家仔细梳理下继承的进阶知识和多态相关知识。

   

  

 

  

  

   

  

  

  

  

时间: 2024-10-06 13:50:42

Python学习之旅—面向对象进阶知识:类的命名空间,类的组合与继承的相关文章

180分钟的python学习之旅

最近在很多地方都可以看到Python的身影,尤其在人工智能等科学领域,其丰富的科学计算等方面类库无比强大.很多身边的哥们也提到Python非常的简洁方便,比如用Django搭建一个见得网站只需要半天时间即可,因此也吸引了我不小的兴趣.之前相亲认识过一个姑娘是做绿色环保建筑设计行业的,提过她们的建筑物的建模也是使用Python,虽然被女神给拒绝了,但学习还是势在必行的,加油. 这部分只涉及python比较基础的知识,如复杂的面向对象.多线程.通信等知识会放在之后的深入学习中介绍,因此整个学习过程也

python学习笔记12-python面向对象

python学习笔记12-python面向对象 python一切皆对象 一.基本概念 1.面向对象和面向过程 面向对象编程:C++,Java,Python 面向过程编程:函数式编程,C程序等 2.类和对象 类:是对事物的抽象,比如:人类,球类 对象:是类的一个实例,比如:足球,篮球,对象就是对类的实例化 属性:五官,眼,鼻子,理解为一个变量,静态属性 方法:对人来说,吃穿住行,理解为一个函数,动态方法 实例说明:球类可以对球的特征和行为进行抽象,然后可以实例化一个真实的球实体出来 3.为什么要使

python学习31(面向对象)

类的两种类型:经典类:class Person():#没有继承objectPass 新式类:class Person(object):#继承objectpass 面向对象技术简介类(Class):用来描述具有相同的属性和方法的对象的集合.它定义了该集合中每个对象所共有的属性和方法.对象是类的实例.类变量:类变量在整个实例化的对象中是公用的.类变量定义在类中且在函数体之外.类变量通常不作为实例变量使用.数据成员:类变量或者实例变量用于处理类及其实例对象的相关的数据.方法重写:如果从父类继承的方法不

C++ Primer 学习笔记_65_面向对象编程 --概述、定义基类和派生类

面向对象编程 --概述.定义基类和派生类 引言: 面向对象编程基于的三个基本概念:数据抽象.继承和动态绑定. 在C++中,用类进行数据抽象,用类派生从一个类继承另一个:派生类继承基类的成员.动态绑定使编译器能够在运行时决定是使用基类中定义的函数还是派生类中定义的函数. 继承和动态绑定在两个方面简化了我们的程序:[继承]能够容易地定义与其他类相似但又不相同的新类,[派生]能够更容易地编写忽略这些相似类型之间区别的程序. 面向对象编程:概述 面向对象编程的关键思想是多态性(polymorphism)

Python学习之旅 —— 基础篇(八)面向对象

概述 面向过程:根据业务逻辑从上到下写垒代码 函数式:将某功能代码封装到函数中,日后便无需重复编写,仅调用函数即可 面向对象:对函数进行分类和封装,让开发“更快更好更强...” 对比示例:函数式变成和面向对象编程 连接数据库的增删改查 # 函数式编程 def slect(host, username, password, sql): pass def create(host, username, password, database): pass def remove(host, usernam

python学习笔记之面向对象、类以及I/O操作

一.I/O 操作: open(name[,mode]) 等价于file(name[,mode]) 模式说明: r 打开只读文件,该文件必须存在. r+ 打开可读写的文件,该文件必须存在. w 打开只写文件,若文件存在则文件长度清为0,即该文件内容会消失.若文件不存在则建立该文件. w+ 打开可读写文件,若文件存在则文件长度清为零,即该文件内容会消失.若文件不存在则建立该文件. a 以附加的方式打开只写文件.若文件不存在,则会建立该文件,如果文件存在,写入的数据会被加到文件尾,即文件原先的内容会被

python学习笔记(七):面向对象编程、类

一.面向对象编程 面向对象--Object Oriented Programming,简称oop,是一种程序设计思想.在说面向对象之前,先说一下什么是编程范式,编程范式你按照什么方式来去编程,去实现一个功能.举个例子,你要做饭,可以用电磁炉,也可以用煤气灶.不同的编程范式本质上代表对各种类型的任务采取的不同的解决问题的思路,两种最重要的编程范式分别是面向过程编程和面向对象编程. 提到面向对象,就不得不提到另一种编程思想,面向过程:什么是面向过程呢,面向过程的思想是把一个项目.一件事情按照一定的顺

Python学习笔记八 面向对象高级编程(二)元类

参考教程:廖雪峰官网https://www.liaoxuefeng.com/wiki/0014316089557264a6b348958f449949df42a6d3a2e542c000 在廖老师的学习网站里"使用元类"这部分还是把我给看晕了...网上搜到一篇感觉讲的相对易懂一些,贴出链接:两句话掌握 Python 最难知识点--元类--以此文作为这篇笔记的源本. "道生一,一生二,二生三,三生万物" 1.在Python世界中,"type"即为道

python(24)- 面向对象进阶

面向对象基础知识: 1.面向对象是一种编程方式,此编程方式的实现是基于对类和对象的使用: 2.类是一个模板,模板中包装了多个‘函数’供使用(可以将多函数中公用的变量封装到对象中): 3.对象,根据模板创建的实例(即:对象),实例用于被包装在类中的函数: 4.面向对象三大特性:封装.继承和多态. 面向对象进阶篇详细介绍python类的成员.成员修饰符和类的特殊成员. 类的成员 类的成员可以分为三大类:字段.方法和属性 注:所有成员中,只有普通字段的内容保存对象中,即:根据此类创建了多少对象,在内存