声明:本内容非盈利性质,也不支持任何组织或个人将其用作盈利用途。本内容来源于参考书或网站,会尽量附上原文链接,并鼓励大家看原文。侵删。
2.1 类与对象
2.1.1 类与对象
(1)类与对象的关系
类是对象的抽象:类为对象提供了统一的抽象结构(模板)。例如:学生,每个学生的年龄、班级、成绩等虽然具体数值不同,但每个学生都有这些特征,而类就提供了这些特征的集合。
对象是类的实例:对象是某个类的具体实现。例如:学生李梅,就是对学生这个类的具体实现。
(2)对象之间的关系
对象之间的关系常的有两种:
- 链接:指的是两个对象之间具有使用与被使用(或操作与被操作)的关系。如:学生使用教室上课,工人操作机器等。
- 聚合:指的是两个对象之间物理上或业务上的包含关系。如:机翼是飞机的一部分,这是物理上的包含;而股票属于股票持有人,这是业务上的包含关系。
(3)类之间的关系
类之间通常有三种基本关系:
- 关联:指两个类之间有某种关系,这种关系不是强制的,是可以随时变更的。如,人可以有一部手机,也可以有多部手机,也可以没有手机。实际中,关联关系通常有三种:一对一、一对多、多对多。
- 继承:特殊的类可以从一般化的类中继承某些结构和行为。如:猫(类)和狗(类)都是宠物(类),也都是哺乳动物(类)。继承关系通常有两种,单继承与多继承。
- 聚合:是类之间的整体与部分的关系。如果对象之间存在聚合关系,那么它们对应的类之间就存在聚合关系。类之间的聚合关系可以是物理上的,如人(类)包含四肢(类),这种物理上的聚合是强聚合,通常称为组合关系;类之间的聚合关系也可以是非物理上的,如宠物(类)包含蛇(类)只是一种人为划分,也可以有人认为不包含,这种非物理上的包含通常还称为聚合关系。聚合与关联最大的不同在于,关联关系的两个类之间是平级的没有从属关系,而聚合关系的两个类之间是要有从属关系的。因此类之间是聚合就一定不是关联,是关联就一定不是聚合。
类之间还有一种关系是依赖:指一个类的元素以某种形式存在于另一个类中,即一个类依赖于另一个类去定义。如学生必定属于某所学校,学校是学生的必要属性,如果没有学校则不能称为学生。
(4)创建类与对象
因为类是对象的模板,所以在高级编程语言的语义已经产生的前提下,类要先于对象创建。python中创建类与对象如下:
'''
创建一个类
'''
class Dog:
petName = 'dog' # 类中定义了nickname属性之后才能使‘print(cls.nickname)’语句不报错
def __init__(self, nickname):
self.nickname = nickname
def run(self):
print('{}在院子里走来走去!'.format(self.nickname))
@classmethod # 定义类方法通过decorater来添加
def test1(cls): # 类方法的系统默认参数为cls即class的简写
print(cls)
print(cls.petName)
# print(cls.nickname) # 这个语句是会报错的,因为nickname是对象的属性,而非类的属性
@staticmethod
def test2(): # 静态方法没有系统指定的默认参数
print('-->静态方法')
# print(self.petName) 静态方法同样不能调用self对象
print(Dog.petName) # 静态方法不能通过cls调用类属性,但可以直接通过类名调用类属性
'''
基于类创建一个对象
'''
d1 = Dog('大黄')
# d2 = Dog() 因为__init__方法中有nickname属性,因此必须赋值,这样会报错
定义在类中的属性和方法都是类的成员。一般属性是指类的特征,而方法是类的行为。
2.1.2 类或对象的特征-属性
在2.1.1(4)的代码示例中,有两个属性petName和nickname。petName是定义在类下面的,称为类属性;而nickname是定义在__init__
方法(初始化方法)下面的,称为实例属性(或对象属性)。
(1)实例属性
实例属性是属于对象的,只有创建了对象之后才有的。因此实例属性可以通过 对象名.属性名
的方式调用,但不能通过 类名.属性名
的方式调用。如下:
# 在2.1.1(4)中创建了对象之后
print(d.nickname) # 这是对的,可以打印出对象的昵称
print(Dog.nickname) # 这样就是错的,类不能调用对象属性
(2)类属性
类属性是属于类的,只要定义了类,类属性就存在。因为类是对象的模板,因此类的属性是所有对象共有的,对象是可以调用的。python中的对象属性是对象特有的,所有不能由类调用。如下:
# 在2.1.1(4)中创建了对象之后,如下两种调用方式都可以打印出类属性
print(d.petName)
print(Dog.petName)
(3)特殊属性
对于所有的类,都有一组特殊属性。这些特殊属性的形式为__valueName__
。如下:
__ name__
:类的名字(字符串)
__ doc __
:类的文档字符串
__ bases __
:类的所有父类组成的元组
__ dict __
:类的属性组成的字典
__ module __
:类所属的模块
__ class __
:类对象的类型
这些属性是每个类默认都有的,不需要我们自己定义的。可以直接由类名称调用,但不能用对象调用:
# 在2.1.1(4)中定义了类之后
print(Dog.__name__) # person
print(Dog.__doc__) # there is doc
print(Dog.__bases__) # (<class '__main__.object_example'>,)
print(Dog.__dir__) # <method '__dir__' of 'object' objects>
print(Dog.__module__) # __main__
print(Dog.__class__) # <class 'type'>
# print(d.__name__) 这样调用是不可以的
(4)属性的内部调用与外部调用
在本小节(1)和(2)中我们打印类属性与对象属性,是在类的外部进行的,这种调用方式称为外部调用。而在2.1.1(4)中我们创建类时,在类内部的__init__
方法中使用了对象属性,在类内部的test1方法中使用了类属性,这两种调用方式都称为内部调用。
内部调用对象属性时,使用的关键字是对象关键字self,self关键字在创建对象后起作用,它指代该对象本身。它可以调用对象属性也可以调用类属性。
内部调用类属性时,使用的关键字是类关键字cls(class的简写),cls关键字在定义类后都起作用,它指代该类。它只能用来调用类属性,不能用来调用类属性。
2.1.3 类或对象的行为-方法
(1)实例方法、类方法与静态方法
实例方法是按照一般定义函数的方式直接定义在类中的方法,它的第一个参数必须是self。2.1.1(4)中的run方法就是一个实例方法。
类方法是在类中定义的函数上加上@classmethod装饰器(相当于java中的注解,后而会详细学习),必须以cls作为第一个参数。2.1.1(4)中的test1方法就是一个类方法。
静态方法是在类中定义的函数上加上@staticmethod装饰器。静态方法主要用来存放一些逻辑性代码,逻辑上属于类,但和类没有很强的关联,一般其中不会涉及对类中属性和方法的操作。2.1.1(4)中的test2方法就是一个静态方法。但要注意,定义静态方法时,不能通过类关键字使用类属性,但可以通过类名来使用类属性;静态方法中不能使用对象属性。因为静态方法是与类同时定义,同时加载到内存的,它先于对象而存在。
(2)魔术方法
python规定了定义在类中的具有特定格式的方法名(__methodName__
)的方法,称为魔术方法。魔术方法在特定时刻自动触发(不同于实例方法、类方法与静态方法,调用时才触发),用于执行某些特定功能。2.1.1(4)例子中的__init__
初始化方法就是一种魔术方法。
魔术方法有很多,我们了解其中常用的几种。
__init__
:初始化方法,也相当于java和C++中的构造方法。触发时机:;参数:必须有self参数,可以自定义其他参数,定义了就要赋值;返回值:无;作用:初始化对象属性;
__del__
:析构方法。触发时机:当对象没有用(没有任何变量引用)的时候被触发(比如使用del object 删除对象时就可以触发);参数:必须有self参数;返回值:无;作用:使用完对象回收资源;
__get__
:触发时机:在获取指定描述符操作的成员属性的值的时候触发;参数:描述符对象本身、描述符描述的属性所在的对象、描述符描述的对象的类;返回值:必须有,不然无法获取相应属性值;
__set__
:触发时机:在设置或者添加指定描述符操作的成员属性的时候触发;参数:描述符对象本身、描述符描述的属性所在的对象、要设置的值;返回值:无;
__len__
:触发时机:使用len(对象)函数时触发;参数:必须有self参数;返回值:整型;作用:返回对象某个方面的数量,如果自定义,可以返回对象成员数量,也可以进行其他操作;
__new__
:触发时机:在实例化对时触发;参数:至少一个cls接收当前类;返回值:必须返回一个对象实例;作用:实例化对象(一般不要使用该方法);
__call__
:触发时机:将对象当作函数调用时触发,如objectName();参数:至少一个self接收对象,其余根据调用时参数决定;返回值:根据情况而定;作用:可以将复杂的步骤进行合并操作,减少调用的步骤,方便使用;
不是所有的魔术方法都需要我们自己定义的,以上除了__init__
方法我们会经常自定义一些内容,其他方法都是系统原来就有的,不太需要自己去重新定义的。
(3)方法的内部调用与外部调用
实例方法的调用:外部调用与实例属性一样,它是属于对象的,不能通过类名来调用,只能通过对象名来调用;内部调用时,实例方法之间可以相互调用,类方法与静态方法均不能调用实例方法,调用时使用self关键字即可。如下:
'''
外部调用
'''
# 在2.1.1(4)中创建了对象之后
d.run() # 可以运行实例方法
# Dog.run() # 错误!不能通过类调用实例方法
'''
内部调用
'''
class Dog:
petName = 'dog'
def __init__(self, nickname):
self.nickname = nickname
def run(self):
print('{}在院子里走来走去!它是{}'.format(self.nickname, self.petName))
def run2(self):
self.run()
类方法:外部调用与类属性一样,它是属于类的,可以通过类名或对象名来调用;内部调用,类方法之间可以通过cls关键字或类名相互调用,静态方法可以通过类名调用类方法(但不能通过cls关键字调用),对象方法不能调用类方法。如下:
'''
外部调用
'''
# 在2.1.1(4)中创建了对象之后,以下两种方式都能执行类方法
Dog.test1()
d.test1()
'''
内部调用
'''
class Dog:
petName = 'dog'
def __init__(self, nickname):
self.nickname = nickname
def run(self):
print('{}在院子里走来走去!它是{}'.format(self.nickname, self.petName))
@classmethod
def test1(cls):
print(cls)
@classmethod
def test11(cls):
cls.test() # 类方法通过cls关键字调用另一个类方法
Dog.test1() # 类方法通过类名调用另一个类方法
print(cls)
@staticmethod
def test2():
Dog.test1() # 静态方法通过类名调用类方法
print('-->静态方法')
print(Dog.petName)
静态方法:外部调用,可以通过类名或对象名来调用;内部调用,类方法中可以调用使用cls关键字或类名类名来调用静态方法,实例方法中可以使用self关键字或类名来调用静态方法,静态方法之间可以通过类名相互调用。如下:
'''
外部调用
'''
# 在2.1.1(4)中创建了对象之后,以下两种方式都能执行静态方法
Dog.test2()
d.test2()
'''
内部调用
'''
class Dog:
petName = 'dog'
def __init__(self, nickname):
self.nickname = nickname
def run(self):
Dog.test2() # 实例方法中通过类名调用静态方法
self.test2() # 实例方法中通过self关键字调用静态方法
print('{}在院子里走来走去!它是{}'.format(self.nickname, self.petName))
@classmethod
def test1(cls):
cls.test2() # 类方法中通过cls关键字调用静态方法
Dog.test2() # 类方法中通过类名调用静态方法
print(cls)
@staticmethod
def test2():
print('-->静态方法')
print(Dog.petName)
@staticmethod
def test21():
Dog.test2() # 静态方法通过类名调用另一个静态方法
print('-->静态方法')
print(Dog.petName)
2.1.4 方法(函数)重载(泛型函数)
java和C++中都有方法(函数)重载的概念。而python本身的基础语法不支持方法重载这个特性,以下代码显示了,无论是形参个数不同还是类型不同的情况,新定义的函数都会将原有定义的同名函数替换掉:
class Teacher:
name = "null"
age = 0
sex = '\\u0000'
subject = "null"
def teach(self,str1, str2, str3):
name = "好学生"
print("{0}教的{4}是:{1}、{2}和{3}".format(self.name, str1, str2, str3, name))
def teach(self,str1):
print("{0}教的是:{1}".format(self.name, str1))
def teach(self,n):
name = "好学生"
print("{0}教的是{1}的学生".format(self.name, n))
lina = Teacher()
lina.name = "lina"
lina.age = 30
lina.sex = '女'
lina.subject = "英语"
lina.teach(4) # 可以正常输出,打印的是最后一个方法
lina.teach("red", "blue", "yellow") # 由于参数列表不兼容,会报错
lina.teach("二班") # 可以正常输出,打印的是最后一个方法
python要实现方法(函数)重载类似的功能需要通过装饰器来实现。在python3.8以后functools库中可以使用@singledispatch装饰器来实现函数重载功能,可以使用@singledispatchmethod装饰器来实现方法重载功能。
使用@singledispatch实现函数重载:
'''
使用@singledispatch实现方法重载
'''
from functools import singledispatch
@singledispatch
def fun(arg):
print(arg)
@fun.register
def _(arg: int):
print(f'arg is int: {arg}')
@fun.register
def _(arg: list):
print(f'arg is list: {arg}')
@fun.register
def _(arg: str):
print(f'arg is str: {arg}')
if __name__ == '__main__':
fun([1,2,3])
fun(1)
fun('str')
# 当对函数进行调用时,先调用@singledispatch装饰的fun函数,@singledispatch判断参数类型(判断第一个参数的),再根据参数类型调用fun函数的某个重载函数(被@fun.register装饰的)。
# 但@singledispatch只通过第一个参数的类型来判断函数是否应该重载,这种形式在python中的术语称为单分派。而对于类中的方法来说,第一个参数总是self,因此不再管用了
使用@singledispatchmethod实现方法重载:
'''
实例方法的重载
'''
from functools import singledispatchmethod
class Dog:
petName = 'dog'
def __init__(self, nickname):
self.nickname = nickname
@singledispatchmethod
def run(self):
print('{}在院子里走来走去!'.format(self.nickname))
@run.register(int)
def _(self, no, name):
print("{}号{}在院子里走来!".format(no, name))
@run.register(str)
def _(self, name):
print("{}在院子里走去!".format(name))
d = Dog("大黄")
# d.run() # 原来方法中不是必须带参的,但是不带参调用会报错,说明重载之后只能走重载函数,不能走原函数
d.run(9527, "大黄")
d.run("大黄")
# @singledispatchmethod也是单分派的,不过取的是self或cls后的第一个参数
'''
类方法的重载
'''
from functools import singledispatchmethod
class Negator:
@singledispatchmethod
@classmethod
def neg(cls, arg):
raise NotImplementedError("Cannot negate a")
@neg.register
@classmethod
def _(cls, arg: int):
return -arg
@neg.register
@classmethod
def _(cls, arg: bool):
return not arg
# 由于调用只能走重载函数,所以一般要在原函数中自定义一个“未实现方法(NotImplementedError)”的异常,要求至少有一个重载函数;
# 如果@singledispatchmethod要用在带其他装饰器(如@classmethod、@staticmethod、@abstractmethod)的函数之上,它必须书写在其他装饰器之上。(即@singledispatchmethod要作为最外层的装饰器)
# 在python3.8之后支持类型表达式。如arg: int,表明该重载方法支持的是int类型的入参;如果要支持两种类型,可以使用表达式arg: int | str。
'''
初始化方法的重载
'''
from functools import singledispatchmethod
class Dog:
petName = 'dog'
@singledispatchmethod
def __init__(self):
raise NotImplementedError("Cannot negate a")
@__init__.register
def _(self, nickname: str):
self.nickname = nickname
@__init__.register
def _(self,no: int, nickname: str):
self.no = no
self.nickname = nickname
def run1(self):
print('{}在院子里走来走去!'.format(self.nickname))
def run2(self):
print('{}编号{}在院子里走来走去!'.format(self.nickname, self.no))
c = Dog("大黄")
c.run1()
d = Dog(9527,"大黄")
d.run2()
@singledispatch与@singledispatchmethod除了所取的单分派参数位置不同,其他的性质基本都相同。
由以上例子可以看出python中的方法(函数)重载与java中的重载有诸多不同,其实python中对这种可以接收不同参数的方法(函数)的正规的称呼是泛型方法(函数),因此python中的方法(函数)重载就是将函数转变为泛型函数。
使用可变参数函数也可以让单个函数大致实现类似方法重载的效果。但是实际使用不建议这样做,因为不灵活,可能会因业务变动而导致较大的改动。