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

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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()方法称之为类的派生方法。


 

 

结语:

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

   

      

 

 

 

  

  

 

   

          

   

 

   

   

  

 














以上是关于Python学习之旅—面向对象进阶知识:类的命名空间,类的组合与继承的主要内容,如果未能解决你的问题,请参考以下文章

Python学习之旅---封装与反射(类的相关知识,面向对象三大特性:继承-多态-封装)

Python学习笔记12(面向对象进阶)

python学习笔记-面向对象进阶&异常处理

python学习笔记-面向对象进阶&异常处理

Python学习之路——Day8(面向对象进阶)

python面向对象进阶版