• 参考书《Python基础教程(第三版)》—— Magnus Lie Hetland
  • 廖雪峰的python教程:​​面向对象编程​

文章目录

  • 一、面向对象编程OOP
  • 1. 基本概念
  • 2. 例子:鸟和云雀
  • 二、类和对象
  • 1. 基本概念
  • 2. 创建一个自定义类
  • 3. 属性、函数和方法
  • 4. 类的命名空间 & 对象的命名空间
  • 5. __init__方法
  • 三、数据封装
  • 1. 读取器和设置器方法
  • 2. “私有属性”
  • 四、继承
  • 1. 普通继承
  • 2. 多继承
  • 3. 深入探讨继承
  • (1)内置方法
  • (2)super方法
  • (3)接口
  • 4. 抽象基类
  • (1)基本概念
  • (2)注册为抽象基类的子类
  • 五、多态
  • 1. 为什么需要多态
  • 2. 多态的威力
  • 3. 开闭原则
  • 4. 鸭子类型
  • 六、获取对象信息

一、面向对象编程OOP

1. 基本概念

  1. OOP是一种程序设计思想。这是一种源自自然界的思想,我们在生活中会把自然地把各种具体事物归类到某种抽象概念。比如我们把“小轿车”、“卡车”、“面包车”等等统称为“车”,一个班级里的“Mike”和“Alice”都是“student”。基于这种思想,OOP中我们先要抽象出Class,再根据Class创建Instance,最后用Instance组成整个程序
  • 类(Class):一种抽象概念,比如我们定义的Class——Student,是指学生这个概念
  1. 对象/实例(object/instance):是类的具现,比如一个个具体的Student
  1. OOP把对象作为程序的基本单元,一个对象包含了
  • 属性:数据
  • 方法:操作数据的函数
  1. 计算机程序视为一组对象的集合,而每个对象都可以接收其他对象发过来的消息,并处理这些消息,计算机程序的执行就是一系列消息在各个对象之间传递。
  2. 在Python中,所有数据类型都可以视为对象,当然也可以自定义对象
  3. 数据封装继承多态是面向对象的三大特点

2. 例子:鸟和云雀

  • 给出一个实例来帮助理解面向对象编程中类的概念及编程方法。这个例子来自《Python基础教程(第三版)》第七章
  • 例如,我们在窗外看到一只鸟,这只鸟就是 “鸟类” 这个非常抽象(通用)的的一个实例
  • 可以把 “鸟类” 视为由所有鸟组成的集合,这个集合有多个子集。 比如我们看到的那只鸟可能属于子集 “云雀”,那么这只鸟不但是 “鸟类” 这个类的一个实例,还是 “云雀” 这个类的一个实例。当一个类的对象为另一个类的子集时,前者就是后置的子类,后者为前者的超类/父类。这个例子中, “云雀” 是 “鸟类” 的子类;“鸟类” 是 “云雀” 的超类。
  • 在面向对象编程中,子类关系意味深长,因为类是由其支持的方法定义的,类的所有实例都有该类的所有方法,因此子类的所有实例都有超类的所有方法。有鉴于此,在定义子类时,只需定义多出来的方法(可能还要修改超类中一些既有的方法)
  • 例如,​​Bird​​​类可能提供方法​​fly​​​,而​​Bird​​​的一个子类​​Penguin​​​类可能新增方法​​eat_fish​​​。由于企鹅不会飞,因此在​​Penguin​​​类的实例中,​​fly​​​方法应该什么都不做或引发异常,这时就要在子类​​Penguin​​​定义时修改父类​​Bird​​​中提供的​​fly​​方法。

二、类和对象

1. 基本概念

  1. 类是抽象的模板,实例是根据类创建出来的一个个具体的 “对象”,每个对象都拥有相同的方法,但各自的数据可能不同
  2. 在较旧的python版本中,"类型"和"类"是泾渭分明的,内置对象是基于类型的,自定义对象是基于类的,可以创建类但不能创建类型。但在python3中,已经没有这种区别

2. 创建一个自定义类

#定义一个person类
class person:
def set_name(self,name):
self.name = name

def get_name(self):
return self.name

def greet(self):
print("Hello,world! I'm {}.".format(self.name))

foo = person() #创建一个person对象,并用foo指向它
bar = person()
foo.set_name("A")
bar.set_name("B")
foo.greet() #打印"Hello,world! I'm A"
bar.greet() #打印"Hello,world! I'm B"
person.greet(foo) #打印"Hello,world! I'm A"
  1. class创建独立的命名空间,用于在其中定义函数
  2. 实例化​​foo = person()​​​的时候,实际上是在内存某个位置创建了一个​​person​​​对象,再把​​foo​​变量指向它。
  3. 通过上面的示例,可以看出self的作用:代指对象本身。如​​foo.set_name("A")​​​时,其实传递了​​foo本身​​​和​​'A'​​两个参数
  4. self参数也可以显示给出,如果​​foo​​​是一个​​person​​​实例,可将​​foo.greet()​​​视为​​person.greet(foo)​​的简写,但后者的多态性更低

3. 属性、函数和方法

  1. 和普通的函数相比,在类中定义的函数只有一点不同,就是第一个参数永远是实例变量self,并且,调用时,不用传递该参数。除此之外,类的方法和普通函数没有什么区别,所以,你仍然可以用默认参数、可变参数、关键字参数和命名关键字参数。
  2. 可以将方法重新关联一个普通函数
  3. 相应的,也可以让普通变量指向类的成员方法,这样如果成员方法有self,它们也可以访问到self
# 将类方法重新关联一个普通函数
class Cla:
def method(self):
print("I have a self")

def func():
print("I dont...")

instance = Cla()
instance.method() #打印I have a self

instance.method = func #重新关联函数(注意这里只写方法名和函数名,不要加括号)
instance.method() #打印I dont...

# 让普通变量指向类的成员方法
class Cla:
I = '1234'
def sing(self):
print(self.I)

def func():
print("I dont...")

inst = Cla()
func = inst.sing #普通函数指向成员函数(关联到类的实例inst)
func() #打印1234(访问self成功)

FUN = inst.sing #普通变量指向成员函数(关联到类的实例inst)
FUN() #打印1234(访问self成功)

4. 类的命名空间 & 对象的命名空间

  1. 在class语句中定义的代码都是在一个特殊的命名空间(类的命名空间)内执行的,而类的所有成员都可以访问这个命名空间
  2. 实例化类的对象时,也会给每个对象一个它自己的对象的命名空间
  3. 当我们定义了一个类属性后,这个属性虽然归类所有,但类的所有实例都可以访问到
  4. 看这个示例
class test:
n = 0 #这个属性n定义在类命名空间。实例化后,每个对象也会在其自己的局部命名空间有一个名为n的属性
def addClass(self):
test.n += 1 #这个方法给类命名空间的n加1

def addSelf(self): #这个方法给对象自己的属性加1
self.n += 1

t1 = test()
t2 = test()
print(t1.n,t2.n,test.n) #打印0 0 0


t1.addClass()
print(t1.n,t2.n,test.n) #打印1 1 1

t1.addSelf() #这里操作了t1的属性n,这会"遮盖"类命名空间中的n,在这之后进行test.n += 1将不会对t1.n产生作用(类似函数形参"遮盖"全局变量)
print(t1.n,t2.n,test.n) #打印2 1 1

t2.addClass()
print(t1.n,t2.n,test.n) #打印2 2 2(可见这里t1.n没有发生变化了,因为t1.n已将test.n遮盖)

t1.addClass()
print(t1.n,t2.n,test.n) #打印2 3 3
  1. 分析:
  • 定义类时,属性n会定义在类的命名空间中
  • 对此类实例化为对象时,对象的命名空间中可以访问到类命名空间的属性n,在“遮盖”发生之前,修改类命名空间的n会改变所有对象的属性n的值
  • 一旦对某个对象的属性n被修改(通过self方法,或者直接进行t1.n=100之类的赋值),这个对象命名空间中,对象的属性n就会“遮盖”类属性n
  • 这类似函数形参“遮盖”全局变量
  • 注意,直到“覆盖”发生前,对象的属性值一直和类的属性值相同
  1. 再看几个对比示例
class test:
#n = 0 #不显式给出属性
def initN(self):
test.n = 0 #间接给出类属性n
self.n = 0 #间接给出对象属性n

def addClass(self):
test.n += 1

def addSelf(self):
self.n += 1

t1 = test()
t2 = test()
t1.initN() #间接定义类属性n和对象属性n,这时直接发生“覆盖”
t2.initN()
print(t1.n,t2.n,test.n) #打印0 0 0

t1.addClass()
print(t1.n,t2.n,test.n) #打印0 0 1

t2.addClass()
print(t1.n,t2.n,test.n) #打印0 0 2

t1.addSelf()
print(t1.n,t2.n,test.n) #打印1 0 2

-------------------------------------
class test:
#n = 0 #不显式给出属性
def initN(self):
test.n = 0
self.n = 0

def addClass(self):
test.n += 1

def addSelf(self):
self.n += 1

t1 = test()
t2 = test()
#t1.initN() #这里注释了一句
t2.initN() #这句执行后,test.n和self.n属性才存在,而且t2对象中发生了遮盖,t1对象没有遮盖(如果这句也注释,n没有定义,下一句会报错test对象没有属性n)
print(t1.n,t2.n,test.n) #打印0 0 0

t1.addClass()
print(t1.n,t2.n,test.n) #打印1 0 1

t2.addClass()
print(t1.n,t2.n,test.n) #打印2 0 2

t1.addSelf()
print(t1.n,t2.n,test.n) #打印3 0 2 到这里t1的n才发生“遮盖”

t2.addClass()
print(t1.n,t2.n,test.n) #打印3 0 3

-------------------------------------
class test:
#n = 0#不显式给出属性
def initN(self):
test.n = 0
self.n = 0

def addClass(self):
test.n += 1

def addSelf(self):
self.n += 1

t1 = test()
t2 = test()
#t1.initN() #不用init方法定义属性n
#t2.initN()
t1.n = 0 #类外定义属性n,这样三个n会分别处于t1、t2和test的命名空间,相互间不影响
t2.n = 0
test.n = 0
print(t1.n,t2.n,test.n) #打印0 0 0

t1.addClass()
print(t1.n,t2.n,test.n) #打印0 0 1

t2.addClass()
print(t1.n,t2.n,test.n) #打印0 0 2

t1.addSelf()
print(t1.n,t2.n,test.n) #打印1 0 2

t2.addClass()
print(t1.n,t2.n,test.n) #打印1 0 3
  1. python不会在执行前对你编写的类检查属性是否存在,python是一个解释型语言,只有解释到调用属性的时候才会去查找对象命名空间或类命名空间中的这个属性,并进行可能的报错。在解释到调用这个属性前,在任意位置定义这个属性都可以避免解释器报错

5. __init__方法

  1. 从上面的可以看出类属性的添加很自由,但因此也有点混乱。我们可以最好定义一个特殊的__init__方法来规范类的属性,这个特殊的方法被称为构造方法
  2. 由于类可以起到模板的作用,因此,可以在创建实例的时候,把一些我们认为必须绑定的属性强制填写进去

class Student(object):
def __init__(self, name, score):
self.name = name
self.score = score

  1. 有了​​__init__​​​方法,在创建实例的时候,就不能传入空的参数了,必须传入与​​__init__​​​方法匹配的参数。​​self​​不需要传,Python解释器自己会把实例变量传进去

>>> bart = Student('Bart Simpson', 59)
>>> bart.name
'Bart Simpson'
>>> bart.score
59

  1. 继承子类中,构造函数的处理:参考​​Python 子类继承父类构造函数说明​

三、数据封装

1. 读取器和设置器方法

  1. 封装:向外部隐藏不必要的细节,这样无需知道对象的构造就能使用它
  2. 一个类的成员变量(属性)应当对类外隐藏,以降低类聚性。如果想要在类外读写类的属性,应该通过读取器和设置器方法。
class test:
I = 1
def getI(self): #读取器方法
return self.I
def setI(self,n): #设置器方法
self.I = n

T = test()
print(T.getI()) #打印1
T.setI(100) #(推荐这种修改方法)
print(T.getI()) #打印100
T.I = 0 #(这种修改方法不好)
print(T.getI()) #打印0
  1. 此类有一个属性I,并对I设置了存取器方法,但由于I没有对类外隐藏,可以从外部直接修改I的值,这样的封装不好,应当将I设置为私有,存取器设置为公共,提升封装性

2. “私有属性”

  1. 在python中没有为私有属性提供直接的支持,我们只能用一些手段来近似做到这一点:在私有成员名称前加两个下划线。这样处理的方法或属性,在类外不能直接访问,而在类内可以
  • 事实上,加两个下划线意义是:对成员进行名称转换,在类内没有变化,在类外必须加"_类名"前缀才能访问,因此
  • 这种方法虽然不能彻底禁止类外访问,但是它发出了强烈的信号,不要这样做!
class test:
__I = 1
def __getI(self):
return self.__I #类内可以访问__I

def getI(self):
return self.__getI() #类内可以访问self.__getI()

def setI(self,n):
self.__I = n

T = test()
print(T.getI()) #打印1
#print(T.__getI()) #报错'test' object has no attribute '__getI'
#print(T.__I) #报错'test' object has no attribute '__I'

#实际上发生了改名
print(T._test__I) #打印1
print(T._test__getI()) #打印1

四、继承

1. 普通继承

  1. 继承:如果已经写了一个类,现在又要写一个很相似的类,可以通过继承继承原有类的方法和属性,并在此基础上修改。
  2. 继承得到的新的class称为子类(Subclass),而被继承的class称为基类、父类或超类(Base class、Super class)
  3. 继承最大的好处是子类获得了父类的全部方法
  4. 可以对子类增加一些方法,也可以修改从父类得到的方法
  5. 当子类和父类都存在同名run()方法时,我们说,子类的run()覆盖了父类的run(),在代码运行的时候,总是会调用子类的run()。这样,我们就获得了继承的另一个好处:多态
#父类
class Animal(object):
def run(self):
print('Animal is running...')

#子类
class Dog(Animal):
pass

#子类
class Cat(Animal):
def run(self):
print('Cat is running...')

dog = Dog()
dog.run() #打印Animal is running...

cat = Cat()
cat.run() #打印Cat is running...

2. 多继承

  1. 多重继承一个子类可能有多个超类/父类
class Talker:
def talk(self):
print("Hi,my value is:",self.value)

class Calculator:
def calculator(self,expression):
self.value = eval(expression)

class TC(Calculator,Talker):
pass

tc = TC()
tc.calculator("1+2*3")
tc.talk() #打印 Hi,my value is:7
  1. 使用多重继承,要注意多个父类的方法不应出现同名否则继承时排在最前的类的方法会“遮住”其他类的同名方法
  1. 假设有父类​​A​​​、​​B​​​、​​C​​​且它们都有方法​​talk()​
  • ​class sonClass(A,B,C)​​​ 这样​​A.talk()​​会遮住其他俩个
  • ​class sonClass(B,A,C)​​​ 这样​​B.talk()​​会遮住其他俩个

3. 深入探讨继承

(1)内置方法

  1. 有以下类
# 这里的Filter类是一个父类,它本身并不过滤任何东西
class Filter:
def init(self):
self.blocked = []

def filter(self,sequence):
return [x for x in sequence if x not in self.blocked]

# SPAMFilter是从Filter继承过来的,它是Filter的一个子类
class SPAMFilter(Filter):
def init(self):
self.blocked = ['SPAM']

s = SPAMFilter()
s.init()
print(s.filter(['SPAM','SPAME'])) #打印['SPAME']
  1. 要确定一个类是否是另一个类的子类:内置方法​​issubclass​

>>> issubclass(SPAMFilter,Filter)
True
>>> issubclass(Filter,SPAMFilter)
False

  1. 有一个类,想知道它的父类:访问类的特殊属性​​__bases__​

>>> print(SPAMFilter.__bases__)
<class '__main__.Filter'>
>>> print(Filter.__bases__)
(<class 'object'>,)

  1. 有一个实列,要确定它是否是某个类的实例:内置方法​​isinstance​
>>> s = SPAMFilter()
>>> isinstance(s,SPAMFilter)
True
>>> isinstance(s,Filter) #间接实例返回也是True
True
>>> isinstance(s,str) #isinstance也可用于类型(如这里的字符串类型str)
False
  1. 有一个对象,想知道它的类:访问对象的特殊属性​​__class__​

>>> print(s.__class)
<class '__main__.SPAMFilter'>

  1. 小结:
  1. 子类对象是父类对象,也就是说在继承关系中,如果一个实例的数据类型是某个子类,那它的数据类型也可以被看做是父类。
  2. 父类对象不是子类对象
  3. 使用isinstance通常不是良好的做法,依赖多态往往是更好的选择。一个重要的例外情况是使用抽象基类和模块​​abc​​时

(2)super方法

  • super方法允许在子类中调用父类的方法,详见:python super详解

(3)接口

  1. 接口即一个类对外暴露的方法和属性,java中常使用接口作为类的蓝图,指明一个类必须要做什么和不能做什么。python中没有接口,人们不会编写显式接口,而是使用 “鸭子类型”,即假定对象能够完成期望的任务,如果不能完成,程序奖失败。这要求对象遵循特定的接口(即实现特定的方法),但如果有需要也可灵活处理,在调用对象方法或属性前检查其是否存在。
  2. 相关内置方法说明

class Bird:
def __init__(self):
self.color = 'red'

def fly(self):
print("{} bird is flying".format(self.color))

bird = Bird()

# 使用hasattr检查对象的属性或方法是否存在
print(hasattr(bird,'fly')) # True
print(hasattr(bird,'color')) # True
print(hasattr(bird,'eat')) # False
print(hasattr(bird,'name')) # False

# 使用callable检查某个属性是否可以调用
print(callable(getattr(bird,'fly',None))) # True
print(callable(getattr(bird,'color',None))) # False

# 使用setattr方法设置对象属性
setattr(bird,'color','blue')
print(bird.color) # blue

  1. 内置方法​​hasattr​​:检查对象的属性或方法是否存在
  2. 内置方法​​callable​​:检查对象的某个属性是否是可调用的(是否是一个方法)
  3. 内置方法​​getattr​​:获取对象的某个属性或方法,可以设置缺省值
  4. 内置方法​​setattr​​:设置对象的某个属性或方法

4. 抽象基类

(1)基本概念

  1. 历史上大多数时候,python几乎都只依赖鸭子类型,偶尔使用​​hasattr​​​检查属性是否存在。与之相比,很多其他语言(比如Java和Go)都使用显式指定接口的理念。很多第三方库提供这种理念的各种实现,最终python通过引入​​abc​​库提供了官方的解决方案,这个模块为所谓的抽象基类提供了支持。
  2. 抽象基类是不能被实例化的类,其职责是定义子类应该实现的一组抽象方法,也可在其中定义普通方法
  3. 使用abc模块定义抽象方法
import abc

# 定义一个抽象类
class AbstractBird(abc.ABC):
def __init__(self):
self.color = 'red'

def eat(self):
print("{} bird is eating".format(self.color))

@abc.abstractmethod
def fly(self):
pass

# 继承抽象类后重写抽象方法,得到一个普通类
class Bird(AbstractBird):
def fly(self):
print("{} bird is flying".format(self.color))

#abstrctBird = AbstractBird() # Can't instantiate abstract class AbstractBird with abstract methods fly

bird = Bird()
bird.eat() # red bird is eating
bird.fly() # red bird is flying
  1. 抽象类的重要特点是不能被实例化
  2. 从抽象类派生的子类,如果没有重写所有抽象方法,就仍是一个抽象类,不能实例化
  3. 抽象类中可以同时定义抽象方法和普通方法

(2)注册为抽象基类的子类

  • ​isinstance​​在处理抽象基类时不符合python通常依赖的 “鸭子类型” 规则,若不加处理,将导致其多态性下降。
  • 以下实例中,​​Duck​​​类实现了​​AbstractBird​​​类的全部方法,根据 “鸭子类型” 规则,​​Duck​​​类的实例应当也是​​AbstractBird​​​类的实例,但是使用​​isinstance​​检查时却不是如此

import abc

# 定义一个抽象类
class AbstractBird(abc.ABC):
def __init__(self):
self.color = 'red'

def eat(self):
print("{} bird is eating".format(self.color))

@abc.abstractmethod
def fly(self):
pass

# 从抽象类派生的普通类
class Bird(AbstractBird):
def fly(self):
print("{} bird is flying".format(self.color))

# 普通类,根据鸭子类型规则,应当属于AbstractBird类
class Duck:
def fly(self):
print('a duck is flying')

bird = Bird()
duck = Duck()

print(isinstance(bird,AbstractBird)) # True
print(isinstance(duck,AbstractBird)) # False

  • 当然,可以从​​AbstractBird​​​类派生出​​Duck​​​类,但​​Duck​​可能是从其他模块引入的,这时就无法使用这种做法。为了解决这个问题,可以将Duck类注册为AbstractBird,这样所有Duck类实例都将被视为AbstractBird对象

# 使用register方法手动注册Duck为AbstractBird
AbstractBird.register(Duck)
print(isinstance(duck,AbstractBird)) # True

  • 这种方法存在一个缺点,​​register​​方法允许用户注册任意类,因此直接从抽象类派生提供的保障没有了,注册的类可能并没有实现被注册类的全部方法。从这个角度看,应当把isinstance返回True视为一种意图表达,注册的类有成为被注册类的意图,但可能会失败。

五、多态

1. 为什么需要多态

  • 假设我们现在要编写一个方法,用于获取商品的价格。我们起初定义使用元组表示商品,结构为 ​​(商品名,价格)​​,那么代码可能会写成
def get_price(object):
return object[1]
  • 后来我们引入了一个新模块,这个模块给出的商品使用字典表示,结构为​​{'price':价格,'name':商品名}​​​。为了兼容这个模块,不得不对以前的方法进行修改,根据上文,可以用​​isinstance​​方法来检查传入参数的类型。
def get_price(object):
if isinstance(object,tuple):
return object[1]
elif isinstance(object,dict):
return object['price']
else:
assert False
  • 随着我们引入更多的模块,传入参数的类型或格式不断变化,我们不得不不断修改​​get_price​​方法,这种方法非常不灵活。
  • 如何解决这个问题呢,我们可以让传入对象自己去做获取价格的操作,而在get_price方法中只需要向传入对象询问价格即可。这正是多态的用武之地,利用多态,上述程序可以简化为
def get_price(object):
return object.get_price()
  • 术语多态源自希腊语,意为“有多种形态”,这大致意味着即使你不知道变量指向的是哪种对象,也能对其进行操作,而且操作的行为随着对象所属的类而异

2. 多态的威力

  • 以下示例来自廖雪峰的教程

#父类Animal(这里是从object类继承,这是python2的写法,python3可以不写object)
class Animal(object):
def run(self):
print('Animal is running...')

#从Animal继承得到3个子类
class Dog(Animal):
def run(self):
print('Dog is running...')

class Cat(Animal):
def run(self):
print('Cat is running...')

class Tortoise(Animal):
def run(self):
print('Tortoise is running slowly...')

#编写一个普通函数,参数是一个animal类型的变量
def run_twice(animal):
animal.run()
animal.run()

#下面说明多态的好处
>>> run_twice(Animal())
Animal is running...
Animal is running...

>>> run_twice(Dog())
Dog is running...
Dog is running...

>>> run_twice(Cat())
Cat is running...
Cat is running...

>>> run_twice(Tortoise())
Tortoise is running slowly...
Tortoise is running slowly...

  • 新增一个​​Animal​​​的子类,不必对​​run_twice()​​做任何修改,实际上,任何依赖Animal作为参数的函数或者方法都可以不加修改地正常运行,原因就在于多态
  • 由于​​Animal​​​类型有​​run()​​方法,因此,传入的任意类型,只要是Animal类或者子类,就会自动调用实际类型的run()方法,这就是多态的意思:
  • 对于一个变量,我们只需要知道它是Animal类型,无需确切地知道它的子类型,就可以放心地调用run()方法
  • 调用方只管调用,不管细节,而当我们新增一种Animal的子类时,只要确保run()方法编写正确,不用管原来的代码是如何调用的。

3. 开闭原则

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

4. 鸭子类型

  1. 对于静态语言(例如Java)来说,如果需要传入Animal类型,则传入的对象必须是Animal类型或者它的子类,否则,将无法调用run()方法。
  2. 对于Python这样的动态语言来说,则不一定需要传入Animal类型。我们只需要保证传入的对象有一个run()方法就可以了
  3. python语言设计类型时遵循了“鸭子原则”:一个对象只要“看起来像鸭子,走起路来像鸭子”,那它就可以被看做是鸭子。因此一个有了​​run()​​​方法的类会被认为是​​Animal​​类型的。即使它并不符合继承体系
  4. 由于这一点,我们可以像下面这样绕过参数必为​​Animal​​类型的限制
class Test():
def run(self):
print("Test")

run_twice(Test()) #打印两遍Test

六、获取对象信息

  • 参考:获取对象信息