类、对象、方法、属性、getter和setter、私有化、运算符重载
1.1 面向过程编程(穷人)
一遇到问题马上想用算法或步骤(逻辑)直接解决问题,使用的工具是:逻辑
1.2 函数式编程(小资)
一遇到问题马上想到的是有没有一个函数具有解决这个问题的功能,如果有就直接用,没有就自己定义一个函数,使用的工具是:函数
1.3 面向对象编程(富人) - 面向生活编程
一遇到问题马上想有没有一个对象具有解决这个问题的功能,如果有就直接用,没有就自己创建一个,使用的工具是:函数
类就是拥有相同功能和属性的对象的集合 - 抽象的概念
对象就是类的实例 - 具体的体现,类的对象(实例)必须能说出具体的一个属性/功能
比如:车
类:车
对象:具体的一辆车(比如你家里的那台车,面前的这辆车)
功能:载人,载货(功能就是对象的方法、函数)
属性:车的颜色、排量、座位数等
注意:汽车、自行车、电动车等都不是一个对象,而是车这个类下面的小分类
用代码描述清楚这个类是拥有哪些相同功能和哪些相同属性的对象的集合
"""
class 类名:
类的说明文档
类的内容
说明:
class - 关键字、固定写法
类名 - 程序员自己命名
要求:是标识符,不是关键字
规范:驼峰式命名并且首写字母大写
见名知义(类的名字:车、人)
不使用系统的函数名、 模块名、类型名
类的说明文档 - 本质就是三个双引号引起来的注释
类的内容 - 主要包括方法(类方法、对象方法、静态方法)和属性(类属性、对象属性)
方法:本质是定义在类里面的函数,是用来对类功能进行描述的
属性:本质是定义在类里面的变量,是用来对类属性进行描述的
"""
class Person:
"""
描述人的功能和属性
"""
def sleep(self): # 这个就是对象的方法
print('人在睡觉!')
构造方法就是构造函数,构造函数和类同名,是专门用来创建对象(实例)的函数,python在创建类的时候会自动创建构造函数(系统隐藏了构造函数中除了__init__方法外的部分,对于构造函数,只能修改__init__方法里面的内容)
构造函数使用以下三个步骤实现创建对象的功能:
1)在调用构造函数时自动调用__new__()
方法创建对象
2)用创建好的对象调用__init_()
方法,对对象进行初始化
3)最后再返回初始化后的对象
"""
构造函数伪代码如下:
def Dog(*args, **kwargs):
对象 = 创建对象(调用__new__函数创建对象)
对象.__init__(*args, **kwargs)
return 对象
"""
class Dog:
def eat(self):
print('狗啃骨头!')
print(f'self指向的对象:{self}')
d1 = Dog()
# Dog()就是一个构造函数
方法:定义在类里面的函数,用来对类功能进行描述
怎么定义:在方法定义前加装饰器@classmethod
怎么调用:对象.方法名()
特点:自带参数self
,用对象调用只有self
参数的方法时不需要传参,系统会自动将当前对象传给self
(谁调用就指向谁)
什么时候用:如果实现方法的功能在不需要使用对象(属性或方法)的前提下又需要使用类属性,那么这个方法就定义成类方法
怎么定义:直接定义在类中
怎么调用:对象.方法名()
特点:自带参数cls
,用类调用只有cls
参数的方法时不需要传参,系统会自动将当前类传给cls
(指向当前对象)
什么时候用:如果实现方法的功能时需要使用对象(属性或方法),那么这个方法就定义成对象方法
# 证明self指向的是当前对象
class Dog:
def eat(self):
print('狗啃骨头!')
print(f'self指向的对象:{self}')
d1 = Dog()
print(f'd1:{d1}')
d1.eat()
# 输出结果:
"""
d1:<__main__.Dog object at 0x00B9D028>
狗啃骨头!
self指向的对象:<__main__.Dog object at 0x00B9D028>
"""
# 可以看到self指向的就是d1这个对象
怎么定义:在方法定义前加装饰器@staticmethod
怎么调用:类.方法名()
特点:定义在类中的普通变量
什么时候使用:如果实现方法的功能在不需要使用对象(属性或方法)的前提下又不需要使用类属性,那么这个方法就定义成类方法
class Person:
num = 100
def __init__(self, n='张三', a=12, g='女'):
self.name = n
self.age = a
self.gender = g
# 定义对象方法
def eat(self):
print(f'{self.name}在吃饭')
# 定义类方法
@classmethod
def show_info(cls):
print(f'人的数量是:{cls.num}')
# 定义静态方法
@staticmethod
def info():
print('这是静态方法')
# 创建对象
p1 = Person('小明', 18, '男')
# 调用对象方法
p1.eat() # 小明在吃饭
# 调用类属性和方法
print(Person.num) # 100
Person.show_info() # 人的数量是:100
# 调用静态方法
Person.info() # 这是静态方法
p1.info() # 这是静态方法
魔法方法:一__
开头和一__
结尾的方法,魔法方法不需要程序员主动调用,系统会在特定情况下自动调用
__init_
方法 作用:用于对象在被创建时定义对象属性
1)每次通过类创建对象时,系统都会自动调用
2)创建对象时构造函数传不传参数传几个参,看__init_
后面除了self
以外跟了多少个参数
3)如果需要给类创建init
方法,保证方法名是__init_
,以及方法的性质是对象方法
# 证明__init__方法会被自动调用
class Person:
def __init__(self):
self.name = ''
print('__init__方法被自动调用')
def sleep(self):
print('人睡觉')
print(f'self指向的对象:{self}')
p1 = Person() # __init__方法被自动调用
# 在创建对象p1后打印了__init__中print里的内容,证明在创建对象时,系统自动调用了__init__方法
__repr__
方法 作用:在打印对象的时候,系统自动调用__repr__
方法,并且自动获取函数返回值,返回值必须是字符串
class Person:
def __init__(self, n, a, g):
self.name = n
self.age = a
self.gender = g
def __repr__(self):
return f'{self.__dict__}' # 返回值必须是字符串
p1 = Person('张三', 18, '男')
# 打印时会自动调用__repr__方法
print(p1) # {'name': '张三', 'age': 18, 'gender': '男'}
类中的属性分为两种:类属性和对象属性
怎么定义:在类里面方法外面定义
怎么调用:类.属性名
什么时候用:属性值不会因为对象的不同而不一样时
怎么定义:以
self.属性名
的形式存在在__init__
方法里
怎么调用:对象.属性名
什么时候用:属性值可能会因为对象的不同而不一样时
# 定义一个类,包含人的姓名、年龄、性别
class Person:
def __init__(self, n='张三', a=12, g='女'):
self.name = n
self.age = a
self.gender = g
def show_info(self):
return f'name:{self.name}, age:{self.age}, gender:{self.gender}'
p1 = Person('小芳')
p2 = Person('小丽', 18)
p3 = Person('小明', 18, '男')
print(p1.show_info())
print(p2.show_info())
print(p3.show_info())
# 输出结果
"""
name:小芳, age:12, gender:女
name:小丽, age:18, gender:女
name:小明, age:18, gender:男
"""
# 练习:定义一个点类,有属性x坐标/y坐标, 方法:以'x:?,y:?'的形式打印点的信息
# 要求创建点对象的时候可以给坐标赋值,如果不赋值x坐标和y坐标都是0和0
# 添加方法:获取当前点到另外一个点的距离
class Point:
def __init__(self, x=0, y=0):
self.x = x
self.y = y
def show_info(self, p):
return ((self.x-p.x)**2+(self.y-p.y)**2)**0.5
p1 = Point()
p2 = Point(3, 4)
result = p1.show_info(p2)
print(f'点({p1.x},{p1.x})和点({p2.x},{p2.x})之间的距离是:{result}')
# 输出结果
"""
点(0,0)和点(3,3)之间的距离是:5.0
"""
内置类属性是通过继承得到的,不需要自己定义,可以直接通过类或者对象进行调用(需要自己调用,魔法方法不需要自己调用)
1)类.__doc__
- 获取类的说明文档
2)类.__module__
- 获取当前类定义所在的模块名
3) 对象.__class__
- 获取当前对象对应的类,和type(对象)
的功能一样
4)类.__name__
- 获取类的类名
5)类.__dict__/对象.__dict__
- 获取当前类 / 对象的所有属性,并且以字典的形式返回
**6)类.__base__
** - 获取类的父类
7).类.__bases__
- 获取类的所有父类,并且以元组的形式返回
class Animal:
"""
这个是Animal类
"""
num = 100
def __init__(self, age='1', gender='雄'):
self.age = age
self.gender = gender
def func1(self):
print(f'这是{self.__class__.__name__}类的对象方法:age: {self.age}, gender: {self.gender}')
class Dog(Animal):
"""这个是Dog类"""
num = 1000
def __init__(self, name='旺财', age='1', gender='雄'):
super().__init__(age, gender)
self.name = name
def func1(self):
print(f'这是{self.__class__.__name__}类的对象方法:age: {self.age}, gender: {self.gender}')
d1 = Dog()
# 类.__doc__ - 继承时不继承父类的说明文档
print(f'类说明文档:{Dog.__doc__}') # 类说明文档:这个是Dog类
# 类.__module__
print(f'类定义所在的模块:{d1.__module__}') # 类定义所在的模块:__main__
# 对象.__class__ - 和type(对象)的作用一样
print(f'对象所属的类:{d1.__class__}') # 对象所属的类:
# 对象.__name__ 类.__name__
print(f'类的类名:{Dog.__name__}') # 类的类名:Dog
print(f'对象的类名:{d1.__class__.__name__}') # 对象的类名:Dog
# 类.__dict__ 对象.__dict__
print(f'类的属性:{Dog.__dict__}') # 类的属性:{'__module__': '__main__', '__doc__': '这个是Dog类', 'num': 1000, '__init__': , 'func1': }
print(f'对象的属性:{d1.__dict__}') # 对象的属性:{'name': '旺财', 'age': '1', 'gender': '雄'}
# 类.__base__ 返回当前类的父类
print(f'类的父类:{Dog.__base__}') # 类的父类:
# 类.__bases__ 以元组的形式返回当前类的所以父类
print(f'类的所有父类:{Dog.__bases__}') # 类的所有父类:(,)
属性: 变量 属性名:字符串
属性的增删查改类似字典的增删查改
1)查
a.对象.属性
b.getattr(对象, 属性名)
区别:getattr更灵活,属性名可以动态传入
2)改/增
a.对象.属性
b.setattr(对象, 属性名)
区别:setattr更灵活,属性名可以动态传入
3)删
a.对象.属性
b.delattr(对象, 属性名)
区别:delattr更灵活,属性名可以动态传入
class Person:
"""人类"""
num = 100
def __init__(self, name='张三', age=18, gender='男'):
self.name = name
self.age = age
self.gender = gender
def __repr__(self):
return f'{self.__dict__}'
p1 = Person('小丽', 18, '女')
print(p1) # {'name': '小丽', 'age': 18, 'gender': '女'}
# 1、查
print(p1.name) # 小丽
# getattr方法可以动态获取对象的属性值(通过输入不同的value值来控制)
value = input('请输入属性:') # 请输入属性:age
print(getattr(p1, value)) # 18
# 两种方法在对象没有该属性时都会报错,区别在于getattr方法可以通过传入一个属性值来补救
# print(p1.height) # 报错
# print(getattr(p1, 'height')) # 报错
print(getattr(p1, 'height', 170)) # 170
# 2、改/增
p1.name = '小花'
print(p1) # {'name': '小花', 'age': 18, 'gender': '女'}
p1.height = 170
print(p1) # {'name': '小花', 'age': 18, 'gender': '女', 'height': 170}
setattr(p1, 'height', 180)
print(p1) # {'name': '小花', 'age': 18, 'gender': '女', 'height': 180}
setattr(p1, 'study_id', 'stu0001') # 这里也可以改成动态输入的形式
print(p1) # {'name': '小花', 'age': 18, 'gender': '女', 'height': 180, 'study_id': 'stu0001'}
# 3、删
del p1.height
print(p1) # {'name': '小花', 'age': 18, 'gender': '女', 'study_id': 'stu0001'}
value = input('请输入属性:') # 请输入属性:study_id
delattr(p1, value)
print(p1) # {'name': '小花', 'age': 18, 'gender': '女'}
什么时候用:在获取属性值之前想要做别的事,就可以给这个属性添加getter
怎么用:
第1步:在需要添加getter的属性前加_
第2步:在装饰器@property后面定义一个函数
函数名是对应的属性不加_
函数没有参数,有一个返回值,返回值就是获取属性值后得到的结果
第3步:通过对象获取属性是时候,属性不需要加_
什么时候用:在给属性赋值之前想要做别的事,就可以给这个属性添加setter,但是用setter的前提是先有getter
怎么用:
第1步:在需要添加setter的属性前加_(但其实不用加了,因为有setter的前提是有getter,在用getter时已经在属性前加了_)
第2步:在装饰器@getter名.setter后面定义一个函数(getter名就是@property后面跟的函数名)
函数名是对应的属性不加_
函数有一个参数,这个参数指向对属性赋值时赋的值,没有返回值
第3步:通过对象给属性赋值的时候,属性前不需要加_
getter和setter的本质就是在调用函数
写一个类,实现在获取类中的birth属性时:
1、如果形如
birth=812839584.24
(时间戳)这样日期格式的属性值时,将其转换成正常人能看懂的日期格式,如2020-1-1 0:0:0
2、如果本身就是正常日期格式非时间戳,就按照原样输出
3、在给对象属性赋值时,输入的日期不能超过当前日期,否则报错
import time
class Person:
def __init__(self):
self._birth = 812839584.24 # 需要添加getter的属性前需要加_
# 转换日期格式,相当于是在获取对象属性前对需要做些什么事,所以要用getter
@property
def birth(self):
if type(self._birth) not in (int, float):
return self._birth
t1 = time.localtime(self._birth)
t2 = time.strftime('%Y-%m-%d %H:%M:%S', t1)
return t2
# 输入日期不能大于当前日期,相当于是在创建对象或者为属性赋值前,需要检查属性值是否符合要求,所以要用setter
@birth.setter
def birth(self, value):
if type(value) is int and value > time.time():
print(f'{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(value))}出生日期不能大于当前日期!')
raise ValueError
elif type(value) is str and time.mktime(time.strptime(value, '%Y-%m-%d %H:%M:%S')) > time.time():
print(f'{time.mktime(time.strptime(value, "%Y-%m-%d %H:%M:%S"))}出生日期不能大于当前日期!')
raise ValueError
self._birth = value
p1 = Person()
print(p1.birth) # 1995-10-05 04:46:24
p1.birth = 1511000000
print(p1.birth) # 2017-11-18 18:13:20
# p1.birth = 11511000000 # ValueError: 2334-10-09 12:00:00出生日期不能大于当前日期!
其实getter和setter方法的本质就是对象在调用函数,只不过这个函数已经在装饰器的作用下转换成了属性,所以在调用时没有加()
公开的 - 公开的属性和方法在类的内部或者外部都可以用,并且可以被继承
保护的 - 保护的属性和方法在类的内部可以用,外部不可以用,可以被继承
私有的 - 私有的属性和方法在类的内部可以用,外部不可以用,不能被继承
python中只有一种访问权限,都是公开的,所谓的私有化其实都是假的。python中的私有化只是一种说明私有化的方法(python的创始人说他相信python使用者的品质,相信大家在说明是私有的情况下就不会去调用),就是一种约定私有化
python私有化:在属性/方法名前面加__
(注意:只能以__
开头, 不能以__
结尾)
class Dog:
"""这个是Dog类"""
num = 1000
__str1 = '这个是是私有类属性'
def __init__(self, name='旺财', age='1', gender='雄'):
self.name = name
self.__age = age
self.gender = gender
def func1(self):
print(f'公有对象方法:这是{self.__class__.__name__}类的对象方法:age: {self.__age}, gender: {self.gender}')
def __func2(self):
print(f'私有对象方法:这是{self.__class__.__name__}类的对象方法:age: {self.__age}, gender: {self.gender}')
@classmethod
def func3(cls):
print(f'公有类方法:这个是{cls}类的类方法')
@classmethod
def __func4(cls):
print(f'私有类方法:这个是{cls}类的类方法')
d1 = Dog()
# 调用私有属性/方法报错(__str1, __age, __func2, __func4这些都是Dog类私有的,不能被直接调用)
print(Dog.num) # 1000
# print(Dog.__str1) # AttributeError: type object 'Dog' has no attribute '__str1'
print(d1.name) # 旺财
# print(d1.__age) # AttributeError: 'Dog' object has no attribute 'age'
d1.func1() # 公有对象方法:这是Dog类的对象方法:name: 旺财, age: 1, gender: 雄
# d1.__func2() # AttributeError: 'Dog' object has no attribute '__func2'
Dog.func3() # 公有类方法:这个是类的类方法
# Dog.__func4() # AttributeError: type object 'Dog' has no attribute '__func4'
前面说了python中只有公有权限,那么为什么这里直接调用又报错,不能调用了呢?其实啊,python中的私有属性/方法只是在这些变量的前又加了_类名
,只要在私有变量前加上_类名
就可以调用私有属性/方法了。(我们可以通过内置属性__dict__
来获取类/对象的所有变量,里面可以看到该类或对象的所有方法/属性,包括公有的和私有的)
print(Dog.__dict__)
# {'__module__': '__main__', '__doc__': '这个是Dog类', 'num': 1000, '_Dog__str1': '这个是是私有类属性', '__init__': , 'func1': , '_Dog__func2': , 'func3': , '_Dog__func4': , '__dict__': , '__weakref__': }
print(d1.__dict__) # {'name': '旺财', '_Dog__age': '1', 'gender': '雄'}
# 从上面的结果可以看得到,Dog类的私有属性'__str1'变成了'_Dog__str1','__func4'变成了'_Dog__func4',Dog的实例d1的'__age'变成了'_Dog__age'
# 直接调用私有属性/方法报错(__str1, __age, __func2, __func4这些都是Dog类私有的,不能被直接调用)
# 但是可以在私有变量前加上 _类名 调用私有变量
print(Dog._Dog__str1) # 这个是是私有类属性
print(d1._Dog__age) # 1
d1._Dog__func2() # 私有对象方法:这是Dog类的对象方法:name: 旺财, aage: 1, gender: 雄
Dog._Dog__func4() # 私有类方法:这个是类的类方法
让子类拥有父类的属性和方法的过程就是继承,继承可以让子类拥有父类所有的属性和方法(除了静态方法)
子类 - 继承者
父类 - 被继承者
"""
class 子类(父类1, 父类2, 父类3, ...):
类的说明文档
类的内容
说明:
1)类名后面没有括号,默认继承object类(object类是所有类的祖先)
class Dog: == class Dog(object):
2)python中的类支持多继承
"""
1)在子类中重新定义
2)如果需要重写,但是又需要父类的功能,可以用super去调用父类的方法(super只能在对象方法和类方法中使用)
继承调用私有属性/方法时和继承调用公有属性/方法一样,只是在私有属性/方法前需要加_类
,不过一般人家是私有的就不要去调用啦
class Animal:
num = 100
def __init__(self, age='1', gender='雄'):
self.age = age
self.gender = gender
def func1(self):
print(f'这是Animal类的公有对象方法')
@classmethod
def func2(cls):
print(f'这是Animal类的公有类方法')
@staticmethod
def func3():
print('这个是Animal类的静态方法')
class Dog(Animal):
"""这个是Dog类"""
# 重新父类的num属性
num = 1000
# 重新父类的__init__属性
def __init__(self, name='旺财', age='1', gender='雄'):
self.name = name
# 继承调用父类的__init__方法
super().__init__(age, gender)
# 重新父类的func1属性
def func1(self):
# 继承调用父类的func1()方法
super().func1()
print(f'这是{self.__class__.__name__}类的对象方法')
# 重新父类的func2属性
@classmethod
def func2(cls):
# 继承调用父类的func2()方法
super().func2()
print(f'这个是{cls.__name__}类的类方法')
@staticmethod
def func3():
# 报错 - 静态方法不能继承调用
# super().func3()
print(f'这是Dog类的静态方法')
def __repr__(self):
return f'{self.__dict__}'
d1 = Dog('小白', '5', '雄')
print(d1) # {'name': '小白', 'age': '5', 'gender': '雄'}
print()
# 调用Dog类的方法,方法里调用了父类的公有方法和私有方法
d1.func1()
print()
"""
这是Animal类的公有对象方法
这是Dog类的对象方法
"""
d1.func2()
print()
"""
这是Animal类的公有类方法
这个是Dog类的类方法
"""
d1.func3() # 这是Dog类的静态方法
练习:
定义一个教师类在Person类的基础上添加:职称和工龄两个属性,
并且要求创建教师对象的时候必须给名字、职称和性别赋值,年龄和工龄可以赋值可以不赋值
class Person:
def __init__(self, name='张三', age=18, gender='男'):
self.name = name
self.age = age
self.gender = gender
class Teacher(Person):
def __init__(self, name, title, gender, age=18, worktime=1):
super().__init__(name, gender, age)
self.title = title
self.worktime = worktime
def __repr__(self):
return f'{self.__dict__}'
# 创建对象时不赋值,报错
# t1 = Teacher() # TypeError: __init__() missing 3 required positional arguments: 'name', 'title', and 'gender'
# 创建对象时只给姓名,报错
# t1 = Teacher('李四') # TypeError: __init__() missing 2 required positional arguments: 'title' and 'gender'
# 创建对象时只给姓名和职称,报错
# t1 = Teacher('李四', '初级教师') # TypeError: __init__() missing 1 required positional argument: 'gender'
# 创建对象时给名字、职称和性别赋值,不报错
t1 = Teacher('李四', '初级教师', '男')
print(t1)
python中在使用不同的运算符的时候,其本质是在调用运算符对应的方法,每个运算符对应的方法名都是固定的。不同类型的数据在调用相同运算符是时候,其实是在调用各自类型中的方法。某个数据类型是否支持某种运算,其实是看这个数据对应的类型中有没有对应的方法。(类型即类)
如果某个数据类型想要进行某种运算(比如+、-等),但是系统不支持,这个时候就需要程序员自己在对应的类中重写该方法,这个过程就叫做运算符重载。
1、写一个人类,属性自定,一般方法自定,要求实现对象的加法(+),运算符具体是什么样的规则自己定
class Person:
"""人类"""
num = 100
def __init__(self, name='张三', age=18, gender='男'):
self.name = name
self.age = age
self.gender = gender
def __repr__(self):
return f'{self.__dict__}'
def __add__(self, other):
return [other, self.__dict__]
p1 = Person()
p2 = Person(name='王五', age=18, gender='男')
p3 = Person(name='李四', age=18, gender='男')
print(p1.__dict__) # {'name': '张三', 'age': 18, 'gender': '男'}
print(p2.__dict__) # {'name': '王五', 'age': 18, 'gender': '男'}
print(p3.__dict__) # {'name': '李四', 'age': 18, 'gender': '男'}
# p1.__add__(p2) 加号前面的作为self,后面的作为other
p = p1 + p2
# 为了好看,将下面的对象属性字典用对象.__dict__来代替
print(p) # [p1.__dict__,p2.__dict__]
print(p3 + p) # [p1.__dict__, p1.__dict__, p1.__dict__]
# 其他的运算符也是同样的道理, 都是自己在类里重写方法,如果不知道该方法+对应的方法是什么:1 + 2 ,按住ctrl左键点击+,其他的运算符一样的规则
系统在定义变量保存数据的时候会自动开辟空间来保存数据(空间的大小根据数据的大小确定)。默认情况下,使用的数据如果之前已经保存过了,系统还是会开辟新的内存空间来保存数据(但是数字和字符串类型除外,即已经保存过的不可变类型的数据,不会重新开辟新空间来保存)。一个数据是否需要释放主要看这个数据的引用计数是否为0,如果引用计数不为0(有引用)数据不会销毁,如果引用计数为0(没有引用)数据会自动销毁。
# 对数字和字符串,已经保存过的就不会再开辟新的空间来保存
a = 10
a_1 = 10
b = 'sg'
b_1 = 'sg'
print(id(a), id(a_1), id(b), id(b_1), id(c), id(c_1)) # 1665529920 1665529920 26343616 26343616 26105448
# 用b来保存已经保存过的[1, 2, 3],a和b的内存地址不一样
a = [1, 2, 3]
b = [1, 2, 3]
print(f'a = [1, 2, 3]: {id(a)}, b = [1, 2, 3]: {id(b)}')
# a = [1, 2, 3]: 45536648, b = [1, 2, 3]: 46282152
引用计数就是计算一个地址有多少个变量指向它。一个变量指向一个地址,就是一个引用,有多少个变量指向同一个地址,这个地址就有多少个引用。那么如何改变一个地址的引用计数呢?有多种方式:
1)删除指向地址的变量
2)修改指向地址的变量的值(重新赋值)
h = [1, 2, 3]
# 通过用变量h赋值的方式增加引用
i = h
j = h
k = h
print(h, i, j, k)
print(id(h), id(i), id(j), id(k))
# 通过为变量赋值的方式减少引用
h = [1, 2, 5]
i = [10, 20, 30]
# 通过删除的方式减少引用
del j
print(h, i, j, k)
print(id(h), id(i), id(j), id(k))
下面这张图片是上述代码在内存中引用的部分过程:
什么是拷贝
拷贝是为变量赋值的一种方式,将一个变量的值传给另一个变量。拷贝在python中有专门的一个模块copy
,用的时候需要先导入from copy import copy, deepcopy
。拷贝又分为浅拷贝和深拷贝
:
相同点:
对不可变对象,不管用什么方式拷贝,变量地址都不会改变;
对可变对象,不管用什么方式拷贝,变量地址都不一样
区别 - 在可变对象里有小序列时(相同点都是前提):
1)浅拷贝:浅层拷贝 - 如果可变对象里有小序列,得到的不同变量的这个小序列的地址是相同的
2)深拷贝:深层拷贝 - 即使有小序列,得到的变量也是指向不同的地址
变量的赋值有3种,分别是:
1)直接赋值:直接赋值后的变量指向的内存地址规则见 9.1.1 的内存管理说明
2)一个变量等于另一个变量赋值:直接将内存地址赋给另一个变量,详情见变量一节变量存储的原理
3)拷贝赋值(浅拷贝、深拷贝):赋值后变量指向的地址规则见上面的浅拷贝和深拷贝的说明
拷贝的用法如下(下面主要介绍基础用法和几种特殊的情况):
from copy import copy, deepcopy
"""
对于不可变类型数据:
1)使用的数据(不可变)如果之前保存过,就不会再开辟新的空间保存该数据
2)不管是用copy还是deepcopy将保存不可以变数据的变量的值赋值给其他变量,变量都会指向同一个地址,原理看1)
"""
a = 10
a_1 = 10
b = 'sg'
b_1 = 'sg'
print(id(a), id(a_1), id(b), id(b_1), id(c), id(c_1)) # 1665529920 1665529920 26343616 26343616 26105448 26105448
d = copy(a)
e = copy(b)
print(id(d), id(e), id(f)) # 1665529920 26343616 26105448
g = deepcopy(a)
h = deepcopy(b)
print(id(g), id(h), id(i)) # 1665529920 26343616 26105448
但是有几个特殊情况:比如如果在不可变序列中有可变子序列, 在可变序列中有可变子序列
from copy import copy, deepcopy
"""
对于不可变类型数据:
1)使用的数据(不可变)如果之前保存过,就不会再开辟新的空间保存该数据
2)不管是用copy还是deepcopy将保存不可以变数据的变量的值赋值给其他变量,变量都会指向同一个地址,原理看1)
"""
tup1 = (1, 2, 3)
tup1_1 = (1, 2, 3)
tup2 = (1, 2, {
'a': 1, 'b': 2})
list1= [1, 2, 3]
list2 = [1, 2, {
'a': 1, 'b': 2}]
print(id(tup1), id(tup1_1), id(tup2), id(list1), id(list2)) # 44848744 44848744 44987816 44987784 45540424
tup1_c = copy(tup1)
tup2_c = copy(tup2)
list1_c = copy(list1)
list2_c = copy(list2)
print(id(tup1_c), id(tup2_c), id(list1_c), id(list2_c)) # 44848744 44987816 45540296 45540456
tup1_d = deepcopy(tup1)
tup2_d = deepcopy(tup2)
list1_d = deepcopy(list1)
list2_d = deepcopy(list2)
print(id(tup1_d), id(tup2_d), id(list1_d), id(list2_d)) # 44848744 45540136 45540360 45540392
# 这里改变的是序列里的子序列,子序列的值改变,拷贝出来的跟子序列地址有关联的变量的值也跟着改变
tup1[2]['b'] = 1000
list2[2]['a'] = 1000
# 重新赋值,相当于减少引用,t2和list2已经不再指向原来的地址,所以之前拷贝出来的对象值不再跟着改变
tup1 = (1, 2, {
'a': 33, 'b': 2})
list2 = [1, 2, {
'a': 33, 'b': 2}]