Skip to content

Class&Instance

封装、继承、多态

Class

类就像模具,用模具制作出来的产品叫做对象

# 不带继承的类
class ClsName:    # 类名
    # 静态字段,通过类访问 print(ClsName.name), 在内存中只保存一份
    name = ''
    age = 0        # public类变量,任何区域都能访问
    _weight = 0    # protected类变量,当前类和子类和同一模块中才能访问
    __height = 0   # private类变量,只有当前类才能访问

    # 构造方法
    def __init__(self,[para1[, para2[, paraN]):
        """ 这里是类文档 """
        # 普通字段,通过实例来访问 print(clsInstance.para1), 每个实例中都保存
        self.para1 = para1
        self.para2 = para2

    # 类方法
    def method(self):
        """ 这里是方法注释 """
        <statement>

    # 类的私有方法
    def __private_method(self):
        <statement>

# 继承了别的类的类
class ClsName(BaseClsName1[, Base2[, Base3[, BaseN]):    # 括号内是这个类所继承的父类
    <statement>

self

这是方法函数的区别之处:不管是什么方法都要有self这个参数,再写其他参数。

这个self是唯一的不可缺少的,它等价于java 和 C++中的this

但是调用方法的时候不需要给它赋值。

当这样调用对象的方法时:myobject.method(arg1,arg2),解释器会解释为MyClass.method(myobject, arg1, arg2)这就是方法的自动传值

类变量 VS 对象变量

类变量或者说静态字段都是一个东西。(类变量 == 静态字段、类属性)

对象变量或者说普通字段也都是一个东西。(对象变量 == 普通字段、实例属性)

顾名思义,类变量就是属于的变量,对象变量就是属于对象(实例)的变量

差别

定义

1
2
3
4
5
6
7
8
9
class Person:
    # 类变量、静态字段、类属性
    eyes = 2
    nose = 1

    def __init__(self, name, age):
        # 对象变量、普通字段、实例属性
        self.name = name
        self.age = age
![](

实例化

# 创建两个对象(实例化)
p1 = Person('Boii', 20)
p2 = Person('Cai', 18)


# 访问静态变量
print(Person.eyes)    # 2 通过类名访问
print(p1.eyes)        # 2 通过对象访问
print(p2.nose)        # 1 通过对象访问

# 访问对象变量
print(p1.name)   # Boii
print(p2.name)   # Cai

修改类变量以后

1
2
3
4
5
# 修改静态变量
Person.eyes = 1

print(p1.eyes)    # 1
print(p2.eyes)    # 1

1
2
3
4
5
# 修改对象变量
p1.age = 50

print(p1.age)    # 50
print(p2.age)    # 18, 改了对象A的,对象B是不受影响的

类变量,是属于类的,存在类那块内存中,类变量被该类创建出来的对象共享。

例如上图中,最上面的表格中就是类本身占的内存,其中就有类变量 eyes 和 nose

下面两个小表格就是对象 p1 和 p2 各自占的空间,其中就保存着对象变量 name,age

修改类变量 eyes 和 nose 后,对象 p1 和 p2 去访问 eyes 和 nose 就会访问到修改后的值

修改对象变量 name 或 age 后,对象之间互不影响。

假设一个类创建了两个对象A 和 B

这时内存中其实是有三块空间的,一块是类的,一块是对象A的,一块是对象B的。

当通过类名.类变量访问的时候,是访问类那块内存里的类变量的

当通过类名.类变量修改的时候,是修改类那块内存里的类变量的

当通过对象.类变量访问的时候,是系统跑去类那块内存里访问类变量的

当通过对象.类变量修改的时候,是系统跑去类那块内存复制 类变量到对象那块内存里的

此时通过类名.类变量修改,再通过对象.类变量访问,其实访问的是对象自己内存里的那个类变量

class Person:
    # 类变量、静态字段
    eyes = 2

    def __init__(self, name, age):
        self.name = name
        self.age = age

# 创建两个对象
p1 = Person('Boii', 20)
p2 = Person('Cai', 18)


# 通过类 访问静态变量
print(Person.eyes)        # 2
# 通过对象 访问静态变量
print(p1.eyes)       # 2
print(p2.eyes)       # 2


### 通过类 修改静态变量
Person.eyes = 1
# 再通过类 访问静态变量
print(Person.eyes)        # 1
# 再通过对象 访问静态变量
print(p1.eyes)       # 1
print(p2.eyes)       # 1



### 通过对象 修改对象变量
p1.eyes = 50

# 再通过类 访问静态变量
print(Person.eyes)        # 1
# 再通过对象 访问静态变量
print(p1.eyes)       # 50    此时personA的内存里已经有eyes这个变量了
print(p2.eyes)       # 1

注意 p1 中 已经多了一个 eyes 的变量了

另外

但是注意,如果静态变量是字典 dict,则不管怎么访问怎么修改,得到的都是一致的

看下面的例子,类变量中有一个字典类型 d

1
2
3
4
5
6
7
8
9
class Person:
    # 类变量、静态字段
    eyes = 2
    d = {1: 'A', 2: 'B'}

    def __init__(self, name, age):
        # 对象变量、普通字段
        self.name = name
        self.age = age

创建两个对象,然后通过对象修改类变量,和通过对象修改字典类型类变量

# 创建两个对象
p1 = Person('Boii', 20)
p2 = Person('Cai', 18)

# 通过类 修改静态变量, 改动的是类空间里的eyes
Person.eyes = 1

# 通过对象 修改静态变量, 是复制一份eyes到对象空间里并修改
p1.eyes = 3

# 通过对象 修改静态字典变量, 并不会复制一份d到对象空间里
p1.d[1] = 50

可以看到通过类名修改后,类空间里的eyes被修改了

通过对象修改类变量之后,对象空间里多了一个 eyes 变量并且已经修改了

通过对象修改字典类型类变量之后,没有复制一份,而是修改了类空间里字典的值

小结

  1. 类变量,又称静态字段、类属性
  2. 对象变量,又称普通字段、实例属性
  3. 类变量是所有对象共有的,对象变量是对象自己独有的
  4. 类变量可以通过 类名.类变量对象.类变量访问
  5. 类变量可以通过 类名.类变量对象.类变量修改
  6. 通过 对象.类变量 修改时,除非该类变量是字典类型,否则都会把类变量复制一份到对象中去

实例方法、类方法、静态方法

实例方法

最普通最常用的方法。类中定义的非私有的方法,每个对象在被创建以后都有自己的实例方法。

  • 定义:第一个参数必须是实例对象,该参数名一般约定为“self”,通过它来传递实例的属性和方法(也可以传类的属性和方法); 如p1.instanceMethod(),Python 解释器会把对象 p1 传给self参数
  • 调用:只能由实例对象调用。
class Person:
    ...
    ...
    # 定义实例方法
    def walk(self, step):
        return f"I walked {step} steps."

p1 = Person()
print(p1.walk(5))        # 调用实例方法

-------------------------------
Output:

I walked 5 steps.

类方法 @classmethod

属于类的方法,和类变量一样,所有对象共享类方法

  • 定义:使用装饰器@classmethod。第一个参数必须是当前类对象,该参数名一般约定为“cls”,通过它来传递类的属性和方法(不能传实例的属性和方法);

    p1.clsMethodPerson.clsMethod,Python解释器会把类 Person 传给 cls 参数

  • 调用:类对象或实例对象都可以调用。

class Person:
    __count = 0

    def __init__(self):
        Person.__count += 1
    ...
    ...
    @classmethod
    def get_count(cls):
        return cls.__count

静态方法 @staticmethod

用来存放逻辑性的代码,逻辑上属于类,但是和类本身没有关系,即,在静态方法中,不会涉及到类中的属性和方法的操作。

可以理解为静态方法是个 独立的、单纯的 函数,仅仅托关于某个类的名称空间中,便于使用和维护。

  • 定义:使用装饰器@staticmethod。参数随意,没有“self”和“cls”参数,但是方法体中不能使用类或实例的任何属性和方法;
  • 调用:类对象或实例对象都可以调用。
class Person:
    __count = 0

    def __init__(self):
        Person.__count += 1
    ...
    ...
    @staticmethod
    def get_time():
        return time.time()

构造方法

def __init__(self)就是类的构造方法

构造方法的形参可以有1~N个。而参数self是必选的首个的,这个self相当于this

self可以换成其他,但是为避免争议和歧义,最好用self

对于一个对象,可以自由的增加属性或者说对象变量、普通字段,但是通过构造方法,可以强制要求在实例化对象时传入必须的参数

class Animal:
    def __init__(self):
        pass

animalA = Animal()    # self 不需要传入
animalA.weigth = 180  # 可以自由的增加对象属性

class Person:
    def __init__(self, name, age):
        pass

personA = Person('Boii', 20)    # 创建对象时必须给 name 和 age

personB = Person()    # !!错误

访问限制

不加下划线,仅变量名/方法名 = public,任何区域都可以访问

一条下划线+变量名/方法名 = protected,当前类子类同一模块才可以访问

两条下划线+变量名/方法名 = private, 当前类才可以访问

Example

# public 示例

class Person:
    def __init__(self, name):
        self.name = name    # public 属性

    def show(self):
        print(self.name)    # 类内部可以访问


person = Person('Boii')

print(person.name)    # Boii 类外部也可以访问
## Person.py
class Person:
    def __init__(self, name):
        self._name = name    # protected 属性

    def show(self):
        print(self._name)     # 当前类中可以访问


person = Person('Boii')

person.show()                 # Boii
## Student.py 
from Person import Person


class Student(Person):
    def __init__(self, name, age):
        super().__init__(name)    # 在子类中通过 super().__init__() 访问
        self.age = age

    def show(self):
        print(f'姓名:{self._name}, 年龄:{self.age}')


student = Student('Boii', 20)

student.show()            # 姓名:Boii, 年龄:20
# private 示例

class Person:
    def __init__(self, name):
        self.__name = name

    def show(self):
        print(self.__name)

    def get_name(self):
        return self.__name

    def set_name(self, name):
        self.__name = name

person = Person('Boii')

print(person.__name)        # 错误!!只能在类中访问私有变量

print(person.get_name())    # Boii, 通过get方法访问

person.set_name('Alice')    # 通过set方法改变

print(person.get_name())    # Alice

private变量实际上是因为 python解释器对外把 __name 改成了 _Person__name,依然可以通过person._Person__name来访问,但是强烈建议不要这么做。

点击查看更多关于访问限制、私有化的问题

封装、继承和多态

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
面向对象三大特性:封装、继承、多态

- **封装**

    把一类东西共通的属性、行为定义在一个类中,就是封装。



- **继承**

    一个类,继承了别的类以后,这个类叫做`子类`

    被继承的类,叫做`基类、父类、超类`

    继承以后,子类就拥有了父类的全部 `非private 功能`

    Python中,子类可以同时继承多个父类



- **多态**

    在子类中编写`与父类同名`的方法,叫做`方法重写`,

    适用于父类功能不能满足子类要求时。这称之为**多态**

    当父类子类的方法相同时,总是会优先调用子类的方法
class Animal:
    def run(self):
        print('Running---')

class Dog(Animal):    # 继承父类 Animal
    pass


class Cat(Animal):    # 继承父类 Animal
    def run(self):    # 方法重写
        print('Cat is Running---')


dog = Dog()
dog.run()    # Running---

cat = Cat()
cat.run()    # Cat is Running---

继承

class ClsName(BaseClass)

在继承中,父类和子类都有的方法(同名的方法),会优先调用子类的;子类没有的,才调用父类的

子类中不定义构造方法__init__(),会调用父类的构造方法__init__()

单继承

单继承 中:如果父类构造方法有参数,则子类必须有构造方法,并调用父类的构造方法且传参

# 子类没有__init__,默认调用父类的__init__
# 父类的__init__没有参数,所以子类可以不写__init__

class Person:
    def __init__(self):
        self.name = "Anonymity"
        self.age = 18


class Student(Person):
    pass

s = Student()
# 父类的__init__有参数,子类必须有__init__,并调用父类的__init__且传参

class Person:
    def __init__(self, name):
        self.name = name


class Student(Person):
    def __init__(self, name, age):
        self.age = age
        super().__init__(name)


s = Student("Boii", 18)

多继承

多继承 中:

  • 如果子类没有__init__(),会调用第一个父类的__init__()
  • 如果第一个父类没有__init__(),会找第二个父类,以此类推...

其中,只要任何一个父类的__init__()有参数,子类就必须有__init__()来调用父类的__init__()

如果有超过一个父类的__init__()有参数,则应该写作 BaseClsName.__init__(self, paras)BaseClsName(type, obj).__init__(paras)

class BaseA:
    def __init__(self, name, age):
        self.name = name
        self.age = age


class BaseB:
    def __init__(self, gender, nationality):
        self.gender = gender
        self.nationality = nationality


class Student(BaseA, BaseB):
    def __init__(self, name, age, gender, nationality):

        BaseA(Student, self).__init__(name, age)

        BaseB.__init__(self, gender, nationality)


s = Student("Boii", 18, "male", "China")

钻石继承

菱形继承是指:子类 sub 继承了 父类 A,B,而父类 A,B又共同继承了祖父类 Base

1
2
3
4
5
     [Base]
    ↗     ↖
  [A]      [B]
    ↖     ↗
     [sub]

由于Python的机制,解释器在创建 sub类对象时会找到 A 的 构造方法,接着找到 Base 的构造方法

然后再找 B 的构造方法,接着找到 Base 的构造方法,这样等于重复调用了 Base 的构造方法

class Base:
    def __init__(self):
        print("Base.__init__")


class A(Base):
    def __init__(self):
        Base.__init__(self)
        print("A.__init__")


class B(Base):
    def __init__(self):
        Base.__init__(self)
        print("B.__init__")


class sub(A, B):
    def __init__(self):
        A.__init__(self)
        B.__init__(self)
        print("sub.__init__")


sub()

# Output:
Base.__init__
A.__init__
Base.__init__
B.__init__
sub.__init__
Base 的 __init__() 被调用了两次

这时候可以改写 sub,A,B 的 __init__()super().__init__

class Base:
    def __init__(self):
        print("Base.__init__")


class A(Base):
    def __init__(self):
        super().__init__()
        print("A.__init__")


class B(Base):
    def __init__(self):
        super().__init__()
        print("B.__init__")


class sub(A, B):
    def __init__(self):
        super().__init__()
        print("sub.__init__")


sub()

# Output:
Base.__init__
B.__init__
A.__init__
sub.__init__

多态的好处

如上示例,多态使得继承之后还可以进行扩展,但是多态还有另一个好处。

示例中,Animal 是父类,Dog 和 Cat 是子类。

Animal 是 Animal 类型;Dog 是 Dog 类型,也是 Animal 类型;Cat 同理

>>> animal = Animal()
>>> dog = Dog()
>>> cat = Cat()

>>> isinstance(animal, Animal)
True
>>> isinstance(dog, Dog)
True
>>> isinstance(dog, Animal)
True
>>> isinstance(cat, Cat)
True
>>> isinstance(cat, Animal)
True
>>> isinstance(animal, Dog)
False

一句话总结就是,对象的类型是自身类+父类,而所有类都自动继承自 object类

因为这个特性,使得python更加灵活

假设现在有个函数,这个函数接受一个animal类型对象

1
2
3
def run_twice(animal):
    animal.run()
    animal.run()

在调用的时候:

>>> animal = Animal()
>>> dog = Dog()
>>> cat = Cat()

>>> run_twice(animal)
Running---
Running---

>>> run_twice(cat)
Cat is Running---
Cat is Running---

>>> run_twice(dog)
Running---
Running---

可以发现,这个接受Animal类型的函数,不仅可以接受Animal类及其子类的对象,还可以根据传入对象的不同实现不同的效果

即使再定义一个类继承Animal,然后创建对象传入 run_twice(),依然可以实现相同的效果,而且不需要改动run_twice()

同理,所有的类都继承自object,如果是run_twice(object),则可以接受任何类型的对象

所以,对于一个变量,只需要知道其父类Animal,就可以放心的使用,在调用时animal.run()是作用在animal还是dog还是cat,由运行时该对象的确切类型决定。

调用方只管调用,不管细节。每当新增一种Animal子类时,只要确保run()方法编写正确,不用管run_twice()怎么实现。

这就是<开闭原则>:

  • 对扩展开放:允许新增Animal子类;
  • 对修改封闭:不需要修改依赖Animal类型run_twice()等函数

多态在静态语言和动态语言中的区别

多态的这种特性在动态语言静态语言中还有些区别.

像java这种静态语言,run_twice(Animal)传入的对象必须是Animal类型或其子类,否则无法调用run()方法。

而python这种动态语言,则不一定要传入Animal类型,只需要保证传入的对象有一个run()方法就可以。

这就是动态语言的“鸭子类型”,它并不要求严格的继承体系,一个对象只要“看起来像鸭子,走起路来像鸭子”,那它就可以被看做是鸭子。

1
2
3
class Timer:
    def run(self):
        print('Start---')

像这个类,没有继承Animal,但依然可以传给run_twice(Animal)

示例:

class Animal(object):   # 编写Animal类
    def run(self):
        print("Animal is running...")

class Dog(Animal):      # Dog类继承Amimal类,没有run方法
    pass

class Cat(Animal):      # Cat类继承Animal类,有自己的run方法
    def run(self):
        print('Cat is running...')
    pass

class Car(object):      # Car类不继承,有自己的run方法
    def run(self):
        print('Car is running...')

class Stone(object):    # Stone类不继承,也没有run方法
    pass

def run_twice(animal):
    animal.run()
    animal.run()

run_twice(Animal())
run_twice(Dog())
run_twice(Cat())
run_twice(Car())
run_twice(Stone())

输出:

Animal is running...
Animal is running...
Animal is running...
Animal is running...
Cat is running...
Cat is running...
Car is running...
Car is running...

AttributeError: 'Stone' object has no attribute 'run'