天池Python训练营Day3 - 对象类函数

Posted 文仙草

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了天池Python训练营Day3 - 对象类函数相关的知识,希望对你有一定的参考价值。

对象、类、函数

1、对象(Object)

1.1 什么是对象?

  • 平时,代码存在硬盘里,cpu只用于运行代码。在运行代码时,使用的数据会临时存储在内存里。
  • cpu具有非常有限的存储空间。为提高运算效率,在cpu和硬盘之间增加内存,用于临时存储马上需要使用的数据。
  • 简单理解,对象即容器,是内存中存储指定数据的区域。整数、小数、布尔值、字符串、空值等都算作对象。

面向对象编程vs面向过程编程

面向对象的编程,是通过对象实现某项功能;面向过程的编程,是将实现该功能分解为一个个步骤,再通过对每个步骤进行抽象进行编程,通过逐一实现每个步骤,最终实现目标功能。

通过生活中的例子来解释:目标:倒1杯白开水,倒1杯果汁。

  • 面向对象:1. 定义对象:杯子*2,白开水,果汁;2. 定义动作(函数):倒水;3. 实现功能:将对象与动作相结合,即给杯子1号倒白开水,给杯子2号倒果汁
  • 面向过程:1. 取出杯子1号;2. 拿起白开水;3. 将白开水倒入杯子1号;4. 取出杯子2号;5. 拿起果汁; 6. 将果汁导入杯子2号。
  • 如果想要改变目标,比如倒2杯果汁,对于面向对象编程,只需要将果汁这个对象替换成白开水,其余不变;但对于面向过程编程,因为步骤5和6变了,导致需要将整个代码重写。
  • 由此可见,面向过程编程的可复用性比较低,难于维护,一个功能的代码只适用于该功能,如果要实现其他功能,即使是很相近的功能,也需重新编写全部代码。
  • 面向对象也有过程步骤(比如倒水),但是关注于对象,将这个对象涉及的过程都存在对象里(比如,将取出杯子这个动作,存在杯子这个对象里)。倒水其实也存在一个对象里,这样可以实现给不同的容器里倒各种饮料。

面向对象的编程思想是将各种功能保存到对应的对象里(例如,杯子是一个对象,倒水是一个对象,果汁也是一个对象),需要实现某个目标的时候,找到并调用需要的对象即可,如果没有现成的对象,则先创造对象再调用对象。面向对象编程更容易维护,也容易复用。

面向对象的三大特性:封装、继承和多态。

  • 封装:隐藏对象中一些不希望被外部访问到的属性或方法。
  • 继承:
  • 多态:

1.2 对象的结构

每个对象都保存三种数据:

  • id:用于标识对象的唯一性,类似于身份证号.

1)id由解析器生成的
2)在CPython中,id是对象的内存地址
3)用id(a)查看对象a的id

k = True
print (id('123') #140443389493616
print (id(k)) #4551017680
print (id(print)) #140443323327584
print (id(id)) #140443323326544
print (id(None)) #4550715496
  • 类型(type): 用于标识对象所属的类型(int, str, float, bool等)。类型决定了对象具有的功能。
  • 值(value): 用于存储对象的值/数据。

注意:

  1. 对象一旦创建,在被删除前,对象的id和type不可以改变
  2. 有些对象的值(value)可以改变,有些对象的value不可以被改变。
  3. 可以改变value的对象称为可变对象,不可以改变value的对象称为不可变对象。整型、字符串、浮点数、布尔值均为不可变对象,即一旦创建了对象"123"、12、123.2、True,则不可以对值本身进行修改,例如不可以将整数12中的1直接改成2。

1.2 变量与对象

  • 变量中存储的不是对象的值,而是对象的id(内存地址)。当使用变量时,实际上是通过对象id在查找对象。
  • 变量中保存的对象,只有在为变量重新赋值时才会改变。
  • 变量和变量之间是相互独立的,修改一个变量不会影响另一个变量。
  • 对象的类型转换:将原对象的值转换成指定的类型,并返回一个新的对象。

1.2.1 改对象与改变量的区别

a = [1,2,3] —> 这个操作是将列表对象的id赋值给变量a

a[0] = 4 ----> 这种操作是通过变量a修改列表对象的值,不修改变量a内所存储的对象id

a = [4,2,3] ----> 这种操作是修改变量a内所存储的对象id,即此时变量a已经指向了新的列表对象[4,2,3]。

原列表对象[1,2,3]仍在内存中存在,但再没有变量指向它了。这个没有变量能找到原列表[1,2,3],就像在太空中孤单游荡的失联宇宙飞船。。。

#【修改对象的值和修改变量的区别】
a = [1,2,3]  #将列表对象[1,2,3]的id赋值给变量a
print (a, 'id=', id(a))
# 运行结果
# [1, 2, 3] id=2604572200832

a[0] = 4  
#上述操作为通过变量a, 修改列表对象的值,使列表对象的值变为[4,2,3]。注意此时变量a仍指向原列表对象,即id(a)未发生变化,只是原列表对象的值改变
print(a, 'id=', id(a))
# 运行结果
# [4, 2, 3] id=2604572200832

a = [4,2,3]  
#上述操作创建了一个新的列表对象[4,2,3],并让变量a指向了新的列表对象,注意id(a)发生了变化。
print(a, 'id=', id(a))
# 运行结果
# [4, 2, 3] id=2604572426880

深拷贝 vs 浅拷贝

当修改对象的时候,如果有其他的变量也指向了同一个对象,则其他指向该对象的变量也会发生变化。

#【修改对象的影响】
a = [1,2,3]   #变量a指向对象[1,2,3]
b = a         #变量a赋值给变量b,即深拷贝,所以变量b也指向对象[1,2,3]
print ('a=', a, ', a_id=', id(a))
print ('b=', b, ', b_id=', id(b))
# 运行结果  a,b的id一样,说明指向同一个对象
#a=[1, 2, 3] , a_id=2604571277760
#b=[1, 2, 3] ,  b_id=2604571277760

b[0] = 10
print ('a=', a, ', a_id=', id(a))
print ('b=', b, ', b_id=', id(b))
# 运行结果   a,b的值都发生了变化,但是id都没有发生变化,因为所指向的对象的值发生了改变
#a=[10, 2, 3] , a_id=2604571277760
#b=[10, 2, 3] , b_id=2604571277760

当修改变量的时候,如果有其他的变量也指向了同一个对象,则其他指向该对象的变量不会发生变化。

#【修改对象的影响】
a = [1,2,3]   #变量a指向对象[1,2,3]
b = a         #变量a赋值给变量b,所以变量b也指向对象[1,2,3]

b = [10,2,3]  #将变量b指向了另一个对象[10,2,3]
print ('a=', a, ', a_id=', id(a))
print ('b=', b, ', b_id=', id(b))
#运行结果  变量b的值和id均发生了变化,而变量a的值和id不受影响
#a= [1, 2, 3] , a_id= 2604572370496
#b= [10, 2, 3] , b_id= 2604572370624
  • 拓展:浅拷贝的应用
#【修改对象的影响--浅拷贝】
a = [1,2,3]   #变量a指向对象[1,2,3]
b = a[:]      #变量a的值赋值给变量b,即浅拷贝,变量b指向新的对象[1,2,3]
print ('a=', a, ', a_id=', id(a))
print ('b=', b, ', b_id=', id(b))
# 运行结果   a,b的值相同,但是id不同
# a= [1, 2, 3] , a_id= 2604572251072
# b= [1, 2, 3] , b_id= 2604572235328
# 这种情况下,修改变量b就不会对对象[1,2,3]产生影响,也不会对变量a产生影响

2. 类(class)

2.1 什么是类

类(class):简单理解为创建对象的图纸,根据类来创建对象,对象是类的实例(instance), 一个类可以创建多个对象。如果多个对象都是通过一个类创建的,则称这些对象为一类对象。

python有内置类,比如int(), str()等,也允许自定义类。可以用type()函数查看对象所属的类。

如何自定义类:用class 关键字创建。语法:class 类名([父类]): 代码块

  • 如果自定义类,类名的开头需要用大写字母。

【示例-创建一个最简单的类】

#【示例-创建一个最简单的类】
class My_Class():
    pass   #此处暂不写任何功能,但空着会认为程序未完结,会报错,所以用pass跳过。
print (My_Class)
# 运行结果
# <class '__main__.My_Class'>    
  • 下图实例中,定义了一个名称为圆形的类,圆形类里有两个特征:半径和颜色(其中颜色是为了更便于区分不同的圆)。圆形类里还定义了两个方法,增加半径长度以及画圆。具体代码如下:
# Create a class Circle

class Circle(object):
    
    # Constructor
    def __init__(self, radius=3, color='blue'):
        self.radius = radius
        self.color = color 
    
    # Method
    def add_radius(self, r):
        self.radius = self.radius + r
        return(self.radius)
    
    # Method
    def drawCircle(self):
        plt.gca().add_patch(plt.Circle((0, 0), radius=self.radius, fc=self.color))
        plt.axis('scaled')
        plt.show()  

2.2 类的属性和方法

在类中可以定义变量和函数。在类中定义的变量,称为属性,将成为这个类的所有实例的公共属性,所有实例都可以访问;在类中定义的函数,称为方法,该类的方法可以通过实例进行访问。

  • 类的属性,类似于日常生活照事物的信息数据(例如人的姓名、身高、年龄等);类的方法,类似于日常生活中事物的行为(例如,人可以说话、走路等)。
  • 类中定义的属性和方法都是公共的,任何该类的实例(instance)都可以访问。
  • 注意,类里定义的属性及该属性对应的值(value)、类里定义的方法都是这个类的值(value), 而不是所创建的对象实例的值(value)。
  • 如果直接打印创建的实例的值,可以发现该实例是没有值的。
#【示例-在类里定义变量和函数】
class Person():
    name = 'Eva'  #定义Person类的属性name为Eva
    birth_year = 1991  #定义Person类的属性birth_year为1991
    age = lambda birth_year, year: year-birth_year
    def say_hello():
        print('hello')
    def say_bye(a): #此处随便定义一个形参,占位,防止报错
        print('bye')
    
p1 = Person()    #创建一个名字为p1的Person类型
print(p1)        #访问p1的值(因为p1没有值,所以返回的其实是type和id)
# 运行结果
# <__main__.Person object at 0x7ff8e5bb1b50>  

调用属性和调用方法的区别:

  • 调用属性:对象.属性,注意不带括号!!
  • 调用方法:对象.函数名(), 注意带括号!!

调用方法(即类里面定义的函数)和调用一般函数的区别:

  • 调用一般函数:调用时有几个函数就会传递几个实参
  • 调用方法:调用时解析器自动传递一个参数。所以方法中至少需要定义一个形参。(该参数到底是什么详见2.2.2 类方法的self)
#【调用属性】
print (p1.name)  #访问这个p1对象的name属性值(注意,name属性值不是p1对象赋予的,不存在p1对象的值里,而是类定义的,存在类的值里)
# 运行结果
# Eva

#【修改p1的name属性】
# 将p1的name属性值修改
# 注意,本次修改实际上是在p1这个对象的值里新增了一个name属性,这个name属性值为'Bob', 类似于对p1对象定义了专属名字。
p1.name = 'Bob' 
print(p1.name)
# 运行结果
# Bob

# 上述修改的是p1对象的name属性,没有修改p1对象所属类的name属性
# 所以,创建一个新的对象p2后,由于p2本身没有定义name的值,所以调用p2的name属性值时仍调用的是类Person里定义的属性值。
p2 = Person()
print(p2.name)
# 运行结果
# Eva

#【调用方法】
p1.say_hello()
# 运行结果,报错,因为类里的函数没有定义形参,但是在调用时会默认传递一个参数
# TypeError: say_hello() takes 0 positional arguments but 1 was given

p1.say_bye() #调用时默认传递一个参数
# 运行结果
# bye

2.2.1 调用对象的属性和方法

属性和方法的查找流程:

  • 当调用一个对象的属性/方法时,解析器会先在当前对象的值(value)里寻找是否含有该属性/方法。
  • 如有,则返回当前对象的属性值/方法;如没有,则去当前对象的类对象里寻找属性值/方法,如有则返回类对象的属性值/方法,如还是没有则返回错误。
  • 类里定义的属性/方法存在类的值里,通过对象定义或者修改后的属性/方法的值存储在对象里
  • 可以理解为,类对象里定义存储的属性值是对该类所有实例均可以调用的普适的属性值,例如将人这个类的年龄属性定义为0,代表所有人出生时年龄为0。对象里定义存储的属性值为当前对象特有的属性值,例如创建一个人后,这个人的默认年龄为人类出生时的年龄,即0岁。如果将这个人的年龄修改为18,表示这个人成长到18岁了。此时,人类的年龄属性仍为0(人类的出生年龄仍是0岁),即后续新创建的没有修改过年龄的人的年龄属性仍为0。

如果某个属性/方法是这个类里所有实例对象共有的,则应在类里定义这个属性/方法;如果某个属性/方法是某个或某几个实例对象特有的,则应该在对象里定义这个属性/方法。

  • 一般情况下,属性保存到实例对象中,方法保存到类中。
  • 因为,一般不同对象的属性信息不一样,但是会有同样的行为(方法)。比如人类都会说话、走路,但是每个人的名字、身高等属性会因人而异。

在类的方法(函数)中,不能直接调用类中定义的属性。

【实例-类的方法不能直接调用类中定义的属性】

#【说明类的方法不能直接调用类中定义的属性】
class Person():
    name = 'Bob'  #在类中定义name属性
    def say_hello(a):  #调用函数时默认传入一个参数
        print ('Hi, my name is %s' %name) #将name属性作为参数变量在函数中调用
p1 = Person()
p1.say_hello()
# 运行结果
# NameError: name 'name' is not defined
# 报错,name没有被定义。

# 将调用的参数变成某个对象的name属性,虽然实际中不会这么使用。
class NewPerson():
    name = 'Bob'
    def say_hello(a):
        print('Hi, my name is %s' %p2.name)
p2 = NewPerson()
p3 = NewPerson()
p2.say_hello()
# 运行结果
# Hi, my name is Bob

#修改p2名字后再尝试调用say_hello函数
p2.name = 'Test'
p2.say_hello()
p3.say_hello()
# 运行结果
# Hi, my name is Test
# Hi, my name is Test

2.2.2 类方法中的self形参解析

针对上文代码中,类中定义的方法可以调用某一个实例对象的属性,其实是因为类方法中,系统默认传递给该形参(即第一个形参)的实参是调用该方法的实例对象。

该默认形参的名称虽然可以随意取名,但是按照习惯通常称这个形参为"self“。

【类的方法中的self形参】

#【self形参的解析】
# 对上文代码中定义的Person类进行修改
class Person():
    age = 0
    def say_hello(a):
        print (a)  #看看参数a的id地址
        print ('Hi, my name is %s' %p1.name)
    def my_age(self):
        print('the info of self is', self)  #看一下参数self的id地址
        print('Hi, I am %s years old.' %self.age)
p1 = Person()
print ('the id of p1 is', id(p1)) #打印一下p1的id信息 
# 运行结果
# the id of p1 is 140376857803088

p1.name = 'Eva'
p1.say_hello() #调用类中定义的say_hello函数
# 运行结果
# the id of a is 140376857803088   
# Hi, my name is Eva
#发现传入实参后a的地址p1的地址一样,说明传入的实参就是p1.


p1.my_age()  #调用类中定义的my_age函数
# 运行结果
# the id of self is 140376857803088
# Hi, I am 0 years old.
# 发现传入实参后self的地址p1的地址一样,说明传入的实参就是p1,而且形参名字的改变不影响传入实参。

2.2.3 类的初始化-特殊方法init

为什么需要类的初始化,即__init__方法?

  • 假如类中有必须设置的属性(比如上述Person类say_hello方法中使用的name属性),但不同对象的该属性的值又不一样(比如每个人的名字不一样)。
  • 为避免创建对象时忘记设置该属性的值导致后续程序报错,需要在对象创建时自动设置该属性;
  • 但同时又不希望在类中直接定义该属性的值,而是希望在创建对象的时候对该属性赋值,从而保持该属性值的灵活性。

__init__是一种特殊方法(也称为魔术方法)。

  • 特殊方法都是以双下划线__开头,以双下划线__结尾。特殊方法会在特殊的时刻自动调用,无需用户手动调用。
  • init方法会在对象创建时马上调用。

【优化Person类,添加特殊方法init】

#【优化Person类,添加特殊方法init】
class Person():
    name = '这是存储在类中的姓名'  #这个name属性存储在Person类对象的value里
    def __init__(self):  #init属于类方法,所以也需要传递形参self
        print ('init方法调用啦')   #测试init方法的调用
        self.name = '这是存储在对象中的姓名'  #这个name属性存储在创建的实例对象的value里
    def my_age(self):
        print('Hi, I am %s years old.' %self.age)
    def say_hello(self):
        print ('Hi, my name is %s' %self.name)
        
p1 = Person()
p1.__init__()     #仅用于说明init可以手动调用,但实际编码中不要这么写,因为init会在特殊时刻自动调用
# 运行结果,发现__init__被调用了两次
# init方法调用啦
# init方法调用啦

print (p1.name)
# 运行结果
# '这是存储在对象中的姓名‘ 

上述代码中,p1 = Person()的实际运行流程

  1. 创建一个名字为p1的变量;
  2. 在内存中创建一个新的对象;
  3. 执行__init__(self)方法,给新的对象初始化属性;
  4. 将对象的id赋值给变量p1。

【继续优化Person类,使用特殊方法init初始化对象属性】

#【优化Person类,使用特殊方法init初始化对象属性】
class Person():
    name = '这是存储在类中的姓名'  #这个name属性存储在Person类对象的value里
    def __init__(self, name):  #init方法里传递name参数
        self.name = name  #这个name是外部传递的参数
    def my_age(self):
        print('Hi, I am %s years old.' %self.age)
    def say_hello(self):
        print ('Hi, my name is %s' %self.name)

p1 = Person()
# 运行结果 (提示传递的参数缺失)
# TypeError: __init__() missing 1 required positional argument: 'name'

p1 = Person('Eva')
p1.say_hello()
# 运行结果
# Hi, my name is Eva

2.2.4 类的基本结构

常用的类的基本结构

class 类名([父类]):

    公共的属性(需对属性赋值)

    #对象的初始化方法
    def __init__(self, [其他形参]):  ([]表示非必须参数)
        self.属性名 = 形参名

    #类的其他方法
    def method_1(self, [其他形参]):
        ......
    def 方法名(self, [其他形参]):
        .......

2.3 类与对象

创建一个对象,即是创建一个类的具体实例。以现实生活类比,人是一个类class, 每一个实体人是一个对象实例。

  • 以python中的内置类为例,int(), float(),str(), list()等都是类。比如, int()是整数类,a=int(10)创建一个int类的实例,等价于 a = 10。

结合2.1中的圆形类实例,以下创建了两个对象,一个是半径10的红色的圆,一个是半径为100的蓝色的圆(注意,因为圆形类里默认对象颜色是蓝色,所以在创建的时候没有传递颜色的参数)

# Create an object RedCircle
RedCircle = Circle(10, 'red')
# Create a blue circle with a given radius
BlueCircle = Circle(radius=100)
  • 获取该对象自带的方法(在圆形类里定义的)
# Find out the methods can be used on the object RedCircle
dir(RedCircle)
  • 获取该对象变量-半径和颜色-的信息
# Print the object attribute radius
RedCircle.radius
RedCircle.color
  • 改变该对象半径变量的值
# Set the object attribute radius
RedCircle.radius = 1
RedCircle.radius
  • 调用该对象自带的方法-画圆
# Call the method drawCircle
RedCircle.drawCircle()
  • 调用该对象自带的方法-增加半径
# Use method to change the object attribute radius
RedCircle.add_radius(2)
print('Radius of object of after applying the method add_radius(2):',RedCircle.radius)
RedCircle.add_radius(5)
print('Radius of object of after applying the method add_radius(5):',RedCircle.radius)

2.4 封装

封装:在定义类的时候,通过一些方式,隐藏对象中一些不希望被外部访问到的属性或方法。

注意,没有方式可以完全隐藏属性或者方法使得外部无法访问,但可以通过一些方式使得这些希望隐藏的属性或方法很难被外部访问到。

隐藏属性的方法:

  • 将对象的属性名改成外部不知道名字,比如在定义类的时候,将属性名改为外部不知道的名字。

【隐藏属性】

#【示例-隐藏属性方法1】
class Dog:
    def __init__(self, name):  #注意init两边各有两个下划线
        self.hidden_name = name   #在类里,属性名是hidden_name
    def say_hello(self):
        print(f'hello, I am self.hidden_name.')
   
d = Dog('旺财') #从外部传入狗的名字
d.say_hello()
# 运行结果
# hello, I am 旺财.

d.name = '小黑'  #除非外部查看类的定义,否则一般很难猜到属性名是hidden_name,所以一旦创建了对象d,则很难从外部直接修改狗的名字
d.say_hello()
# 运行结果
# hello, I am 旺财.

d.hidden_name = '小白'  #但如果真找出来了,通过属性名还是可以从外部修改对象的名字的。 
d.say_hello()
# 运行结果
# hello, I am 小白.

在隐藏了属性后,有时候外部还是会需要获取属性,则通过专门预留的通道进行访问,即使用getter、setter方法。

  • getter方法泛指用于获取属性值的方法,这种方法通常采用”get_属性名“命名;
  • setter法泛指用于修改属性值的方法,这种方法通常采用”set_属性名“命名。
#【在类中设置getter和setter方法】
class Person:
    def __init__(self, name, age):
        self.hidden_name = name
        self.hidden_age = age
    def get_name(self):  #返回对象的name属性值
        return self.hidden_name
    def get_age(self):   #返回对象的age属性
        return self.hidden_age
    def set_name(self, name): #修改对象的name属性
        self.hidden_name = name
        print(f'reset self.hidden_name\\'s age to age.')
    def set_age(self, age): #修改对象的age属性,并添加校验规则
        if int(age) >= 0:
            self.hidden_age = age
        else:
            print('age属性值需为不小于0的整数')
p1 = Person('Eva', 18)
print(p1.get_name())
print(p1.get_age())
# 运行结果
# Eva
# 18
p1.set_age(10)
print(p1.get_age())
# 运行结果
# reset Eva's age to 10.
# 10

p1.set_age(-10)
print(p1.get_age())
# 运行结果
# age属性值需为不小于0的整数
# 18

使用了封装后,增加了类的定义的复杂程度,但是增强了数据的安全性。一是,隐藏了属性,使得调用者无法从外部随意修改对象的属性;二是,增设专门用于获取和修改属性的专用通道,可以控制属性的可读性和值的范围。