python语言基础-2 面向对象-2.1 类与对象

声明:本内容非盈利性质,也不支持任何组织或个人将其用作盈利用途。本内容来源于参考书或网站,会尽量附上原文链接,并鼓励大家看原文。侵删。

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中的方法(函数)重载就是将函数转变为泛型函数。

使用可变参数函数也可以让单个函数大致实现类似方法重载的效果。但是实际使用不建议这样做,因为不灵活,可能会因业务变动而导致较大的改动。

  • 7
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值