一、类和对象
1.1 类和对象
- 在编程世界里,一切都是对象(object)。对象都有属性和行为,当我们把具备了共同特征的对象的属性(静态特征)和行为(动态特征)抽取出来,就可以定义一个类(class)。
- 任何对象都包含在某个类中,一个类可以包含多个对象,归属于同一类中的对象会具备某些共同的属性或行为。
- 我们可以用class关键字来定义类,类的首字母需要大写,类名后跟冒号,如:class Fruit: 。
1.2 属性
- 属性描述的是类的静态特征。
- 类的第一个方法通常是__init__(self):,我们在这个方法中定义类的属性。
1.3 行为
- 行为描述的是类的动态特征。
- 在类中,我们可以用函数来定义“方法”,用此来将类的动态特征描述出来。
class Student:
def __init__(self,name,age,grade):
# 类属性
self.name = name
self.age = age
self.grade = grade
self.school = '明德小学' # 直接定义一个每个对象相同的属性
# self.__age = age
# 类方法
def course(self,name,course_name):
# 三种格式化方法
print('%s暑假学习了%s' % (self.name,course_name))
# print(f'{self.name}暑假学习了{course_name}')
# print('{}暑假学习了{}'.format(self.name,course_name))
def film(self):
# 在类方法中调用类属性
if self.grade == 7:
print(f'{self.name}今年在{self.school}读{self.grade}年级,{self.grade}年级的学生看《三傻大闹宝莱坞》')
elif self.grade == 8:
print(f'{self.name}今年在{self.school}读{self.grade}年级,{self.grade}年级的学生看《绿皮书》')
elif self.grade == 9:
print(f'{self.name}今年在{self.school}读{self.grade}年级,{self.grade}年级的学生看《肖申克的救赎》')
1.4 创建和使用对象
- 创建对象时,需要定义__init__(self):方法中的全部参数,否则会报错。
- 使用对象又叫给对象发消息,或者调用某某方法,即调用类中定义的函数
def main():
# 创建一个对象,指定name,age,grade
stu1 = Student('张三',12,7)
stu2 = Student('李四',12)
# 缺少grade参数会报错:
# TypeError: Student.__init__() missing 1 required positional argument: 'grade'
# 在编程语境中,面向对象的编程思维中,“给对象发消息”指的是调用对象的行为或方法。
# 这是通过给对象发送一个消息(即调用一个方法或函数),对象在接收到这个消息后,会自动完成相应的行为或操作
# 给对象发送film消息=调用对象的film方法
stu1.film() # 张三今年7年级,7年级的学生看《三傻大闹宝莱坞》
# 给对象发送course消息
stu1.course('张三','数学') # 张三暑假学习了数学
stu1.course('王五','语文') # 张三暑假学习了语文
# 因为是给对象stu1发送的消息,所以输出的name还是张三
# 查看对象属性
print(stu1.name)
print(stu1.age)
if __name__ == '__main__':
main()
二、访问可见性
2.1 访问权限
- 在Python中,对象属性和方法的访问权限只有两种:公开的和私有的。
- 如果我们直接定义属性和方法,什么都不做,那对象的属性是外界可以访问的。
- 如果我们想设为私有,即不允许外界访问,只需要在属性和方法前面加上双下划线"__",该属性在类外部将无法访问到。
- 访问私有属性或方法时会报错“ AttributeError: 'xxxx' object has no attribute 'xxxx' ”。
# 属性和方法都公开
class Student:
def __init__(self,name):
self.name = name
def course(self,course_name):
print('%s暑假学习了%s' % (self.name,course_name))
def main():
stu1 = Student('张三')
stu1.course('数学') # 张三暑假学习了数学
print(stu1.name) # 张三
# 属性私有,报错
class Student:
def __init__(self,name):
# 设为私有
self.__name = name
def main():
# 创建一个对象
stu1 = Student('张三')
print(stu1.__name) # AttributeError: 'Student' object has no attribute 'name'
# 方法私有,报错
class Student:
def __init__(self,name):
self.name = name
def __course(self,course_name):
print('%s暑假学习了%s' % (self.name,course_name))
def main():
stu1 = Student('张三')
stu1.__course('数学') # AttributeError: 'Student' object has no attribute '__course'
- 私有的属性在类中可以正常访问,只是在外部访问会报错。
class Student:
def __init__(self,name):
self.__name = name
def course(self,course_name):
print('%s暑假学习了%s' % (self.__name,course_name)) # 调用私有属性self.__name
def main():
stu1 = Student('张三')
stu1.course('数学') # 内部正常访问__name:张三暑假学习了数学
# print(stu1.__name) # 外部访问报错AttributeError
if __name__ == '__main__':
main()
2.3 受保护
- 在实际开发中,我们并不建议将属性设置为私有的,因为这会导致子类无法访问。
- 所以大多数Python程序员会遵循一种命名惯例就是让属性名以单下划线开头来表示属性是受保护的。
- 虽然受保护的属性在本类之外的代码可以访问,但可以提醒操作者,在访问这样的属性时应该要保持慎重。
- 这种做法并不是语法上的规则,单下划线开头的属性和方法外界仍然是可以访问的,所以更多的时候它是一种提醒。如:
class Student:
def __init__(self,name):
self._name = name
def _course(self,course_name):
print('%s暑假学习了%s' % (self._name,course_name))
def main():
stu1 = Student('张三') # 张三暑假学习了数学
stu1._course('数学') # 张三
print(stu1._name)
if __name__ == '__main__':
main()
- 事实上,即使是私有的属性和方法,在外部也并不是完全不可访问的,在访问时只要在私有属性和方法前加“单下划线+类名”,如“_Student”,私有属性和方法就可以被访问到了。
class Student:
def __init__(self,name):
self.__name = name
def __course(self,course_name):
print('%s暑假学习了%s' % (self.__name,course_name))
def main():
stu1 = Student('张三')
# 在类外访问私有属性和方法
stu1._Student__course('数学') # 张三暑假学习了数学
print(stu1._Student__name) # 张三
if __name__ == '__main__':
main()
三、面向对象的三大支柱
面向对象有三大支柱:封装,继承,多态。
3.1 封装
3.1.1 什么是封装
- 引用骆昊大佬的话,封装就是“隐藏一切可隐藏的细节,只向外界提供简单的编程接口”。
- 我们在类中定义的方法将所有数据和对数据的操作都封装起来,在我们创建了对象后,只需要对对象发送一个信息(调用方法),就可以执行方法中的代码,而不必知道方法中的代码。
- 也就是说,我们给对象发送信息时,只需要知道方法的名称和必要的参数(方法的外部视图),而不必知道方法的内部细节(方法的内部视图)。
3.1.2 @property装饰器
- 我们之前学习了将属性设为私有和受保护的,但我们会面临一个问题:如果属性私有(__),在子类外不可访问,它的可用性将降低;如果属性只是受保护(_),这是一种非硬性约束,用户仍然可以在类外对受保护属性进行读取、修改等操作,安全性无法保障。因此,属性的安全性和可用性无法兼得。
- 如果我们想更安全的实现属性的可读但不可修改,可以使用@property的getter方法。此外,@property提供的setter方法(@属性名.setter),使得我们可以对属性允许如何修改进行限制,如:限制修改值不得为负。这是使用公开和受保护属性做不到的。
- 简单的用一张图表示:
属性设置 | 表示 | 操作限制 | 特点 |
公开 | 属性 | 类外可访问,可修改,无限制 | 安全无保障 |
受保护 | _属性 | 类外可访问,可修改,仅做慎重操作提醒,实际无限制 | 安全无保障 |
私有 | __属性 | 类外不可访问,不可修改 | 类外不可用 |
@property | getter | 可访问(如果不定义@setter方法属性只读不可修改) | 可灵活设置只读还是可修改,推荐 |
setter | 可修改 |
因此,使用@property,可以同时保证属性的安全性和可用性。
安全性上,@property可以:① 设置属性前的数据验证,如:限制属性修改时值需要符合的规则 ② 控制访问,如:控制属性完全私有还是只读(即使属性本身私有也可以设置为只读)
可用性上,@property可以:① 简化访问,将方法转化为属性,使得方法可以像属性一样访问,使代码更简洁易读 ② 保持行为一致,在 getter 和 setter 方法中添加额外的逻辑,如验证、类型转换等,不会影响到外部代码的调用方式
我们用一个银行账户的案例来说明,通过方法读取和修改银行账户的存款余额,并进行存取款
class BankAccount:
"""
name是账户名,balance是账户余额
"""
def __init__(self,name,balance):
self._name = name
self._balance = balance
"""balance可读取,getter方法,如果不进行setter设置该属性只读"""
@property
def balance(self):
return self._balance
"""balance属性可修改,setter方法"""
@balance.setter
def balance(self,value):
"""对属性进行修改,修改的值必须符合给定条件"""
"""首先必须是个数,其次不能为负数"""
if not isinstance(value,(int,float)):
raise ValueError("balance must be a number.")
elif value < 0:
raise ValueError("balance cannot be negative.")
else:
self._balance = value
# 存款行为
def deposit(self,value):
"""存款值需要满足一定条件"""
if not isinstance(value,(int,float)):
raise ValueError("deposit amount must be a number.")
elif value < 0:
raise ValueError("deposit amount cannot be negative.")
else:
self._balance += value
return self._balance
def withdraw(self,value):
"""取款额不能大于存款"""
if not isinstance(value,(int,float)):
raise ValueError("withdrawal amount must be a number.")
elif value < 0:
raise ValueError("withdrawal amount cannot be negative.")
elif value > self._balance: # 类内部可以直接调用_balance属性
raise ValueError("insufficient funds")
else:
self._balance -= value
return self._balance
def main():
# 定义一个对象
account1 = BankAccount('user',1000.0)
# 用@property的getter方法读取对象的账户余额
print(account1.balance)
# 用setter方法修改,如果未定义@balance.setter,则balance只读,不可修改
account1.balance = 1200
print(account1.balance)
# account1账户存款,向对象发送deposit消息
print(account1.deposit(300.0))
# account1账户取钱,向对象发送withdraw消息
print(account1.withdraw(300.0))
# 直接读取对象的受保护余额(非法读取)
# print(account1._balance)
# 直接修改对象的受保护余额(非法修改)
# account1._balance = 1200 # 受保护情况下仍然可读可修改
# print(account1._balance) # 无实际保护作用
if __name__ == '__main__':
main()
如果我们希望balance的安全性更高,将balance从受保护属性改为私有属性,仍然可以通过@property读取和修改:
# 将balance设为私有
class BankAccount:
def __init__(self,name,balance):
self.__name = name
self.__balance = balance
@property
def balance(self):
return self.__balance
@balance.setter
def balance(self,value):
if not isinstance(value,(int,float)):
raise ValueError("balance must be a number.")
elif value < 0:
raise ValueError("balance cannot be negative.")
else:
self.__balance = value
def main():
# 定义一个对象
account1 = BankAccount('user',1000.0)
print(account1.balance) # 1000 仍可读取
account1.balance = 1200
print(account1.balance) # 1200 仍可修改
# 直接读取对象的私有余额
# print(account1.__balance) # AttributeError:私有属性无法读取,但可以通过@property修改
# 直接修改对象的私有余额
# account1.__balance = 1200
# print(account1.__balance) # AttributeError:私有属性无法修改,但可以通过setter修改
if __name__ == '__main__':
main()
3.1.3 类属性、实例属性
在学习__slots__变量之前,我们要了解两个概念:类属性和实例属性。
类属性:
类是与类本身相关的属性,不是只跟某个实例相关的属性。
类的全部实例共享类属性,类属性在定义类时即被定义。
类属性可以通过类名和实例名进行访问,修改,但通过类名修改的类属性作用于全部实例,通过实例名修改的仅作用于修改了的实例,不影响类和其他实例。
class Student:
school = 'xiwangxiaoxue' # 类变量、类属性
def __init__(self, name):
self._name = name
def main():
stu1 = Student('xiaoming')
# 通过类名访问类属性
print(Student.school) # xiwangxiaoxue
# 通过实例名访问类属性
print(stu1.school) # xiwangxiaoxue
# 通过类名修改类属性
Student.school = 'mingchengxiaoxue'
print(Student.school) # mingchengxiaoxue
print(stu1.school) # mingchengxiaoxue
# 通过实例名修改类属性
stu1.school = 'xiwangxiaoxue'
print(stu1.school) # xiwangxiaoxue
print(Student.school) # mingchengxiaoxue
"""
可以看出,通过类名修改的类属性作用于类和全部实例;
通过实例修改的类属性只作用于修改的实例,其他实例和类不受影响
"""
if __name__ == '__main__':
main()
实例属性
实例属性是与类对象相关的属性,在对象初始化时用__init__方法定义。
实例属性中的实例(instance)和对象在Python上是同一个概念,对象就是实例。
每个对象都可以有其自己的一组属性值。
class Student:
def __init__(self,name,age): # 实例变量
self.name = name # 实例属性name
self.age = age # 实例属性age
"""
编程中,类变量、类属性,实例变量、实例属性通常指一个概念
"""
def main():
stu1 = Student('xiaoming',12)
stu2 = Student('xiaohong',11)
# 通过类名访问实例属性
print(Student.name) # AttributeError: type object 'Student' has no attribute 'name'
# 通过实例名访问实例属性
print(stu1.name) # xiaoming
# 通过实例名修改实例属性
stu1.age = 13
print(stu1.age) # 13
# 添加实例属性grade,id
stu1.grade = 7
print(stu1.grade) # 7
stu2.id = 123
print(stu2.id) # 123
"""
可以看出,无法通过类名访问实例属性,但可以通过实例名访问、修改实例属性;
此外,可以看到,实例属性可以随意添加
"""
if __name__ == '__main__':
main()
可以看到,当不做限制,我们可以动态的给对象添加任意个新属性。
在实际开发中,这显然是容易导致问题和不利于维护的。
所以,我们可以使用__slots__解决可以随意添加新属性的问题。
3.1.4 __slots__
__slots__是 Python 中的一个特殊类属性,它的主要作用有:
1、限制动态属性。__slots__限制实例只能拥有__slots__中列出的属性,不能添加之外的属性。
2、减少内存占用。使用__slots__可以避免为每个实例创建__dict__字典,而是直接在对象内部为指定的属性分配固定大小的空间。这样可以减少内存使用
3、更快的属性访问。由于属性直接存储在对象内部的固定位置,因此访问这些属性通常比通过字典查找更快。举例:
class Student:
"""__slots__定义了一个包含字符串的列表,每个字符串代表一个实例属性的名称"""
__slots__ = ['name','age','grade']
def __init__(self,name,age): # 实例属性name,age
self.name = name
def main():
stu1 = Student('xiaoming',12)
# 添加实例属性grade,id
stu1.grade = 7
print(stu1.grade) # 7
"""
stu1.id = 123
print(stu1.id) # AttributeError: 'Student' object has no attribute 'id'
id未被slots定义,所以不能被添加,而grade可以"""
if __name__ == '__main__':
main()
3.1.5 静态方法、类方法
1、静态方法
- 格式:
@staticmethod
def 方法名([可选参数]):
方法主体
- 作用:
- 不依赖对象实例。因为静态方法属于类本身,不属于类的实例,所以可以在创建实例前访问它,或者不创建实例直接访问
- 工具函数。静态方法常用作工具函数,执行一些与类状态无关的运算,如数学运算。类状态包含类变量,实例变量和方法,静态方法的运行与类状态无关,仅受类本身定义影响。
- 性能优化。执行相对实例方法更快,因为静态方法的执行不涉及实例的创造和初始化。
- 示例:
class Calculate: """创建静态方法,此处用作工具函数""" @staticmethod def add(a,b): return a + b @staticmethod def minus(a,b): return a - b def main(): # 可以不创建对象直接调用 print(Calculate.add(2,3)) # 5 print(Calculate.minus(2,3)) # -1 # 也可以创建对象后,用对象名调用 x = Calculate() print(x.add(5,4)) # 9 print(x.minus(5,4)) # 1 if __name__ == '__main__': main()
2、类方法
- 格式
@classmethod
def fuc(cls,[其它参数]):
方法主体
- 作用
- 类方法通过装饰器
@classmethod
来定义,并且它们的第一个参数通常是cls
,代表类本身,而不是实例对象。 - 这使得类方法可以在没有创建类实例的情况下调用,并且可以访问和修改类级别的状态。
- 示例:
class Calculate: variable = 0 # 定义一个;类方法,修改类变量variable @classmethod def var(cls,value): cls.variable += value return cls.variable # 调用类方法 print(Calculate.var(10))
3.1.6 对比:实例方法、类方法、静态方法
在Python中,实例方法、类方法和静态方法是三种不同的方法类型,它们具有不同的用途和调用方式。
类别 | 特点 |
实例方法 | 1、实例方法是与类的实例相关联的方法。 2、第一个参数总是表示实例本身,通常命名为 3、可以通过类的实例(对象)来调用这些方法。 4、它们可以访问和修改实例(对象)的属性。 |
类方法 | 1、类方法与类本身相关联,而不是与类的实例相关联。 2、第一个参数是类本身,通常命名为 3、使用 4、可以通过类名直接调用,也可以通过实例调用(但不建议这样做)。 5、它们可以访问和修改类属性,但不能直接访问实例属性,除非明确传入一个实例作为参数。 |
静态方法 | 1、静态方法与类没有特殊关联,它们只是定义在类的命名空间中的普通函数。 2、不需要特殊的第一个参数(如 3、使用 4、可以通过类名或实例来调用。 5、它们不能访问类的属性或实例的属性,除非这些属性作为参数明确传入。 |
总结 | 1、实例方法与对象的状态有关,可以访问和修改对象的属性。 2、类方法与类本身的状态有关,可以访问和修改类的属性。 3、静态方法与类和对象的状态无关,它们更像是属于类的命名空间中的普通函数。 4、在选择使用哪种方法时,应该考虑我们想要实现的功能以及是否需要访问或修改类的状态。 |
3.1.7 总结:类封装的方法
方法 | 用途 |
私有属性 | 属性完全不可访问,不可修改(__) |
@property装饰符 | 控制属性是否可访问,可修改(setter) |
__slots__ | 控制可添加的属性范围 |
静态方法 | 不依赖实例或类,常用于执行与类本身相关但不依赖于类实例或类状态的操作,常放置于类内部 |
接《Day6 面向对象编程(二)》