大一第三次培训

面向对象概述

面向对象编程(Object-oriented Programming,简称 OOP),是一种封装代码的方法。基本特性是:封装、继承、多态。

在前面的学习中,我们已经接触了封装。比如说,将各种类型的数据扔进列表中,这就是一种简单的封装,是数据层面的封装;把常用的代码块打包成一个函数,这也是一种封装,是语句层面的封装。封装,其实就是隐藏实现功能的具体代码,仅留给用户使用的接口。

面向对象的思想可以更好地模拟真实世界里的事物(将其视为对象),并把描述特征的数据和代码块(函数)封装到一起。从某种程序上,相比较只用变量或只用函数,使用面向对象的思想可以更好地模拟现实生活中的事物。
下面是一个形象的例子:
若用程序模拟一个乌龟,应该如何来实现呢?使用面向对象的思想会更简单,可以分为如下两个方面进行描述:
从表面特征来描述,例如,绿色的、有 4 条腿、重 10 kg、有外壳等等。
从所具有的的行为来描述,例如,它会爬、会吃东西、会睡觉、会将头和四肢缩到壳里,等等。
如果将乌龟用代码来表示,则其表面特征可以用变量来表示,其行为特征可以通过建立各种函数来表示。

	class tortoise:
	    bodyColor = "绿色"
	    footNum = 4
	    weight = 10
	    hasShell = True
	    #会爬
	    def crawl(self):
	        print("乌龟会爬")
	    #会吃东西
	    def eat(self):
	        print("乌龟吃东西")
	    #会睡觉
	    def sleep(self):
	        print("乌龟在睡觉")
	    #会缩到壳里
	    def protect(self):
	        print("乌龟缩进了壳里")

面向对象相关术语
在系统学习面向对象编程之前,初学者要了解有关面向对象的一些术语。当和其他人讨论代码的时候,或者尝试查找我们遇到的问题的解决方案时,知道正确的术语会很有帮助。

面向对象中,常用术语包括:
:可以理解是一个模板,通过它可以创建出无数个具体实例。比如,前面编写的 tortoise 表示的只是乌龟这个物种,通过它可以创建出无数个实例来代表各种不同特征的乌龟(这一过程又称为类的实例化)。
对象:类并不能直接使用,通过类创建出的实例(又称对象)才能使用。这有点像汽车图纸和汽车的关系,图纸本身(类)并不能为人们使用,通过图纸创建出的一辆辆车(对象)才能使用。
属性:类中的所有变量称为属性。例如,tortoise 这个类中,bodyColor、footNum、weight、hasShell 都是这个类拥有的属性。
方法:类中的所有函数通常称为方法。不过,和函数所有不同的是,类方法至少要包含一个 self 参数(后续会做详细介绍)。例如,tortoise 类中,crawl()、eat()、sleep()、protect() 都是这个类所拥有的方法,类方法无法单独使用,只能和类的对象一起使用。

封装

类和类的定义

类仅仅充当图纸的作用,本身并不能直接拿来用,而只有根据图纸造出的实际物品(对象)才能直接使用。因此,Python 程序中类的使用顺序是这样的:
创建(定义)类,也就是制作图纸的过程;
创建类的实例对象(根据图纸造出实际的物品),通过实例对象实现特定的功能。

本节先教大家如何创建(定义)一个类,如何使用定义好的类将放到后续章节进行讲解。
Python类的定义
Python 中定义一个类使用 class 关键字实现,其基本语法格式如下:

	class 类名:
	    多个(≥0)类属性...
	    多个(≥0)类方法...

注意,无论是类属性还是类方法,对于类来说,它们都不是必需的,可以有也可以没有。另外,Python 类中属性和方法所在的位置是任意的,即它们之间并没有固定的前后次序。

__init__()构造方法

在创建类时,我们可以手动添加一个 __init__() 方法,该方法是一个特殊的类实例方法,称为构造方法(或构造函数)。

构造方法用于创建对象时使用,每当创建一个类的实例对象时,Python 解释器都会自动调用它。Python 类中,手动添加构造方法的语法格式如下:

	def __init__(self,...):
	    代码块

注意,此方法的方法名中,开头和结尾各有 2 个下划线,且中间不能有空格。

Python 中很多这种以双下划线开头、双下划线结尾的方法,都具有特殊的意义,后续会一一为大家讲解。

另外,__init__() 方法可以包含多个参数,但必须包含一个名为 self 的参数,且必须作为第一个参数。也就是说,类的构造方法最少也要有一个 self 参数。
添加构造方法的代码如下所示:

	class TheFirstDemo:
	    '''这是一个学习Python定义的第一个类'''
	    #构造方法
	    def __init__(self):
	        print("调用构造方法")
	    # 下面定义了一个类属性
	    add = 'hahahaha'
	    # 下面定义了一个say方法
	    def say(self, content):
	        `print(content)

注意,即便不手动为类添加任何构造方法,Python 也会自动为类添加一个仅包含 self 参数的构造方法。
仅包含 self 参数的 __init__() 构造方法,又称为类的默认构造方法。

在上面代码的后面,顶头(不缩进)直接添加如下代码:

	mydemo = TheFirstDemo()

这行代码的含义是创建一个名为 mydemo 的 TheFirstDemo 类对象。
运行代码可看到如下结果:

调用构造方法

显然,在创建 mydemo 这个对象时,隐式调用了我们手动创建的 __init__() 构造方法。

不仅如此,在 __init__() 构造方法中,除了 self 参数外,还可以自定义一些参数,参数之间使用逗号“,”进行分割。例如,下面的代码在创建 __init__() 方法时,额外指定了 2 个参数:

	class PythonLanguage:
	    '''这是一个学习Python定义的一个类'''
	    def __init__(self,name,add):
	        print(name,"的网址为:",add)
	#创建 add 对象,并传递参数给构造函数
	add = PythonLanguage("百度","www.baidu.com")

注意,由于创建对象时会调用类的构造方法,如果构造函数有多个参数时,需要手动传递参数,传递方式如代码中所示。

运行以上代码,执行结果为:

百度 的网址为: www.baidu.com

可以看到,虽然构造方法中有 self、name、add 3 个参数,但实际需要传参的仅有 name 和 add,也就是说,self 不需要手动传递参数。
关于 self 参数,后面会做详细介绍,这里只需要知道,在创建类对象时,无需给 self 传参即可。

类对象的创建和使用

通过前面章节的学习,我们已经学会如何定义一个类,但要想使用它,必须创建该类的对象。

创建类对象的过程,又称为类的实例化。

对已定义好的类进行实例化,其语法格式如下:

	类名(参数)

定义类时,如果没有手动添加 __init__() 构造方法,又或者添加的 __init__() 中仅有一个 self 参数,则创建类对象时的参数可以省略不写。

定义的类只有进行实例化,也就是使用该类创建对象之后,才能得到利用。

总的来说,实例化后的类对象可以执行以下操作:

访问或修改类对象具有的实例变量,甚至可以添加新的实例变量或者删除已有的实例变量;
调用类对象的方法,包括调用现有的方法,以及给类对象动态添加方法。

类对象访问变量或方法
使用已创建好的类对象访问类中实例变量的语法格式如下:

	类对象名.变量名

使用类对象调用类中方法的语法格式如下:

	对象名.方法名(参数)

注意,对象名和变量名以及方法名之间用点 “.” 连接。

给类对象动态添加/删除变量
Python 支持为已创建好的对象动态增加实例变量,方法也很简单,举个例子:

	# 为clanguage对象增加一个money实例变量
	clanguage.money= 159.9
	print(clanguage.money)

运行结果为:

159.9

可以看到,通过直接增加一个新的实例变量并为其赋值,就成功地为 clanguage 对象添加了 money 变量。

既然能动态添加,那么是否能动态删除呢?答案是肯定的,使用 del 语句即可实现,例如:

	#删除新添加的 money 实例变量
	del clanguage.money
	#再次尝试输出 money,此时会报错
	print(clanguage.money)

运行程序会发现,结果显示 AttributeError 错误:

Traceback (most recent call last):
File “C:/Users/mengma/Desktop/1.py”, line 29, in
print(clanguage.money)
AttributeError: ‘CLanguage’ object has no attribute ‘money’

给类对象动态添加方法
Python 也允许为对象动态增加方法。以本节开头的 Clanguage 类为例,由于其内部只包含一个 say() 方法,因此该类实例化出的 clanguage 对象也只包含一个 say() 方法。但其实,我们还可以为 clanguage 对象动态添加其它方法。

需要注意的一点是,为 clanguage 对象动态增加的方法,Python 不会自动将调用者自动绑定到第一个参数(即使将第一个参数命名为 self 也没用)。例如如下代码:

	# 先定义一个函数
	def info(self):
	    print("---info函数---", self)
	# 使用info对clanguage的foo方法赋值(动态绑定方法)
	clanguage.foo = info
	# Python不会自动将调用者绑定到第一个参数,
	# 因此程序需要手动将调用者绑定为第一个参数
	clanguage.foo(clanguage)  # ①
	# 使用lambda表达式为clanguage对象的bar方法赋值(动态绑定方法)
	clanguage.bar = lambda self: print('--lambda表达式--', self)
	clanguage.bar(clanguage) # ②

上面的第 5 行和第 11 行代码分别使用函数、lambda 表达式为 clanguage 对象动态增加了方法,但对于动态增加的方法,Python 不会自动将方法调用者绑定到它们的第一个参数,因此程序必须手动为第一个参数传入参数值,如上面程序中 ① 号、② 号代码所示。

self参数

在定义类的过程中,无论是显式创建类的构造方法,还是向类中添加实例方法,都要求将 self 参数作为方法的第一个参数。例如,定义一个 Person 类:

	class Person:
	    def __init__(self):
	        print("正在执行构造方法")
	    # 定义一个study()实例方法
	    def study(self,name):
	        print(name,"正在学Python")

事实上,Python 只是规定,无论是构造方法还是实例方法,最少要包含一个参数,并没有规定该参数的具体名称。之所以将其命名为 self,只是程序员之间约定俗成的一种习惯,遵守这个约定,可以使我们编写的代码具有更好的可读性(大家一看到 self,就知道它的作用)。

self 使类的实例方法具备了操作类实例对象的属性的能力。

打个比方,如果把类比作造房子的图纸,那么类实例化后的对象是真正可以住的房子。根据一张图纸(类),我们可以设计出成千上万的房子(类对象),每个房子长相都是类似的(都有相同的类变量和类方法),但它们都有各自的主人,那么如何对它们进行区分呢?
当然是通过 self 参数,它就相当于每个房子的门钥匙,可以保证每个房子的主人仅能进入自己的房子(每个类对象只能调用自己的类变量和类方法)。
如果你接触过其他面向对象的编程语言(例如 C++),其实 Python 类方法中的 self 参数就相当于 C++ 中的 this 指针。

也就是说,同一个类可以产生多个对象,当某个对象调用类方法时,该对象会把自身的引用作为第一个参数自动传给该方法,换句话说,Python 会自动绑定类方法的第一个参数指向调用该方法的对象。如此,Python解释器就能知道到底要操作哪个对象的方法了。

因此,程序在调用实例方法和构造方法时,不需要手动为第一个参数传值。例如,更改前面的 Person 类,如下所示:

	class Person:
	    def __init__(self):
	        print("正在执行构造方法")
	    # 定义一个study()实例方法
	    def study(self):
	        print(self,"正在学Python")
	zhangsan = Person()
	zhangsan.study()
	lisi = Person()
	lisi.study()

上面代码中,study() 中的 self 代表该方法的调用者,即谁调用该方法,那么 self 就代表谁。因此,该程序的运行结果为:

正在执行构造方法
<__main__.Person object at 0x0000021ADD7D21D0> 正在学Python
正在执行构造方法
<__main__.Person object at 0x0000021ADD7D2E48> 正在学Python

另外,对于构造函数中的 self 参数,其代表的是当前正在初始化的类对象。

值得一提的是,除了类对象可以直接调用类方法,还有一种函数调用的方式,例如:

	class Person:
	    def who(self):
	        print(self)
	zhangsan = Person()
	#第一种方式
	zhangsan.who()
	#第二种方式
	who = zhangsan.who
	who()#通过 who 变量调用zhangsan对象中的 who() 方法

运行结果为:

<__main__.Person object at 0x0000025C26F021D0>
<__main__.Person object at 0x0000025C26F021D0>

显然,无论采用哪种方法,self 所表示的都是实际调用该方法的对象。
总之,无论是类中的构造函数还是普通的类方法,实际调用它们的谁,则第一个参数 self 就代表谁。

类变量和实例变量(类属性和实例属性)

无论是类属性还是类方法,都无法像普通变量或者函数那样,在类的外部直接使用它们。

我们可以将类看做一个独立的空间,则类属性其实就是在类体中定义的变量,类方法是在类体中定义的函数。

前面章节提到过,在类体中,根据变量定义的位置不同,以及定义的方式不同,类属性又可细分为以下 3 种类型:

类体中、所有函数之外:此范围定义的变量,称为类属性或类变量;
类体中,所有函数内部:以“self.变量名”的方式定义的变量,称为实例属性或实例变量;
类体中,所有函数内部:以“变量名=变量值”的方式定义的变量,称为局部变量。

类变量(类属性)
类变量指的是在类中,但在各个类方法外定义的变量。举个例子:

	class Person:
		avg_age = 19  # 平均年龄
		
		def __init__(self, in_name, in_age):
			self.s_name = in_name
			self.s_age = in_age
			
		def say(self):
			print("大家好,我是", self.s_name)

上面程序中, avg_age 就属于类变量。

类变量的特点是,所有类的实例化对象都同时共享类变量,也就是说,类变量在所有实例化对象中是作为公用资源存在的。类方法的调用方式有 2 种,既可以使用类名直接调用,也可以使用类的实例化对象调用。

因为类变量为所有实例化对象共有,通过类名修改类变量的值,会影响所有的实例化对象。

注意,通过类对象是无法修改类变量的。通过类对象对类变量赋值,其本质将不再是修改类变量的值,而是在给该对象定义新的实例变量(在讲实例变量时会进行详细介绍)。

值得一提的是,除了可以通过类名访问类变量之外,还可以动态地为类和对象添加类变量。例如,在 Person 类的基础上,添加以下代码:

	Person.avg_height = 170 # 平均身高

实例变量(实例属性)
实例变量指的是在任意类方法内部,以“self.变量名”的方式定义的变量,其特点是只作用于调用方法的对象。另外,实例变量只能通过对象名访问,无法通过类名访问。

还是那个例子:

	class Person:
		avg_age = 19  # 平均年龄
		
		def __init__(self, in_name, in_age):
			self.s_name = in_name
			self.s_age = in_age
			
		def say(self):
			self.have_sayed = True # 已经说过话了
			print("大家好,我是", self.s_name)

此 Person 类中,s_name、s_age 和have_sayed都是实例变量。其中,由于 __init__() 函数在创建类对象时会自动调用,而 say() 方法需要类对象手动调用。因此,Person 类的类对象都会包含 s_name 和 s_age 实例变量,而只有调用了 say() 方法的类对象,才包含 have_sayed 实例变量。

前面讲过,通过类对象可以访问类变量,但无法修改类变量的值。这是因为,通过类对象修改类变量的值,不是在给“类变量赋值”,而是定义新的实例变量。

类中,实例变量和类变量可以同名,但这种情况下使用类对象将无法调用类变量,它会首选实例变量,这是不推荐“类变量使用对象名调用”的原因之一。

另外,和类变量不同,通过某个对象修改实例变量的值,不会影响类的其它实例化对象,更不会影响同名的类变量。例如:

	class CLanguage :
	    name = "xxx"  #类变量
	    add = "http://"  #类变量
	    def __init__(self):
	        self.name = "百度"   #实例变量
	        self.add = "http://www.baidu.com"   #实例变量
	    # 下面定义了一个say实例方法
	    def say(self):
	        self.catalog = 13  #实例变量
	clang = CLanguage()
	#修改 clang 对象的实例变量
	clang.name = "python"
	clang.add = "http://python.com"
	print(clang.name)
	print(clang.add)
	clang2 = CLanguage()
	print(clang2.name)
	print(clang2.add)
	#输出类变量的值
	print(CLanguage.name)
	print(CLanguage.add)

程序运行结果为:

python
http://python.com
百度
http://www.baidu.com
xxx
http://

不仅如此,Python 只支持为特定的对象添加实例变量。例如,在之前代码的基础上,为 clang 对象添加 is_free 实例变量,实现代码为:

	clang.is_free = True
	print(clang.is_free)

局部变量
除了实例变量,类方法中还可以定义局部变量。和前者不同,局部变量直接以“变量名=值”的方式进行定义,例如:

	class Person:
		avg_age = 19  # 平均年龄
		
		def __init__(self, in_name, in_age):
			self.s_name = in_name
			self.s_age = in_age
			
		def say(self):
			mark = 100
			print("大家好,我是", self.s_name)

mark就是局部变量
通常情况下,定义局部变量是为了所在类方法功能的实现。需要注意的一点是,局部变量只能用于所在函数中,函数执行完成后,局部变量也会被销毁。

实例方法、静态方法和类方法

和类属性一样,类方法也可以进行更细致的划分,具体可分为类方法、实例方法和静态方法。

和类属性的分类不同,对于初学者来说,区分这 3 种类方法是非常简单的,即采用 @classmethod 修饰的方法为类方法;采用 @staticmethod 修饰的方法为静态方法;不用任何修改的方法为实例方法。
其中 @classmethod 和 @staticmethod 都是函数装饰器,后续会对其做详细介绍。

接下来就给大家详细的介绍这 3 种类方法。
Python类实例方法
通常情况下,在类中定义的方法默认都是实例方法。前面章节中,我们已经定义了不只一个实例方法。不仅如此,类的构造方法理论上也属于实例方法,只不过它比较特殊。

比如,下面的类中就用到了实例方法:

	class CLanguage:
	    #类构造方法,也属于实例方法
	    def __init__(self):
	        self.name = "百度"
	        self.add = "http://www.baidu.com"
	    # 下面定义了一个say实例方法
	    def say(self):
	        print("正在调用 say() 实例方法")

实例方法最大的特点就是,它最少也要包含一个 self 参数,用于绑定调用此方法的实例对象(Python 会自动完成绑定)。实例方法通常会用类对象直接调用,例如:

	clang = CLanguage()
	clang.say()

运行结果:

正在调用 say() 实例方法

当然,Python 也支持使用类名调用实例方法,但此方式需要手动给 self 参数传值。例如:

	#类名调用实例方法,需手动给 self 参数传值
	clang = CLanguage()
	CLanguage.say(clang)

运行结果为:

正在调用 say() 实例方法

Python类方法
Python 类方法和实例方法相似,它最少也要包含一个参数,只不过类方法中通常将其命名为 cls,Python 会自动将类本身绑定给 cls 参数(注意,绑定的不是类对象)。也就是说,我们在调用类方法时,无需显式为 cls 参数传参。
和 self 一样,cls 参数的命名也不是规定的(可以随意命名),只是 Python 程序员约定俗称的习惯而已。

和实例方法最大的不同在于,类方法需要使用@classmethod修饰符进行修饰,例如:

	class CLanguage:
	    #类构造方法,也属于实例方法
	    def __init__(self):
	        self.name = "百度"
	        self.add = "http://www.baidu.com"
	    #下面定义了一个类方法
	    @classmethod
	    def info(cls):
	        print("正在调用类方法",cls)

注意,如果没有 @classmethod,则 Python 解释器会将 fly() 方法认定为实例方法,第一个参数无论是什么都会认定为“self”。

类方法推荐使用类名直接调用,当然也可以使用实例对象来调用(不推荐)。例如,在上面 CLanguage 类的基础上,在该类外部添加如下代码:

	#使用类名直接调用类方法
	CLanguage.info()
	#使用类对象调用类方法
	clang = CLanguage()
	clang.info()

运行结果为:

正在调用类方法 <class ‘__main__.CLanguage’>
正在调用类方法 <class ‘__main__.CLanguage’>

Python类静态方法
静态方法,其实就是我们学过的函数,和函数唯一的区别是,静态方法定义在类这个空间(类命名空间)中,而函数则定义在程序所在的空间(全局命名空间)中。

静态方法没有类似 self、cls 这样的特殊参数,因此 Python 解释器不会对它包含的参数做任何类或对象的绑定。也正因为如此,类的静态方法中无法调用任何类属性和类方法。
静态方法需要使用@staticmethod修饰。
静态方法的调用,既可以使用类名,也可以使用类对象。
在实际编程中,几乎不会用到类方法和静态方法,因为我们完全可以使用函数代替它们实现想要的功能,但在一些特殊的场景中(例如工厂模式中),使用类方法和静态方法也是很不错的选择。

python类调用实例方法

通过前面的学习,类方法大体分为 3 类,分别是类方法、实例方法和静态方法,其中实例方法用的是最多的。我们知道,实例方法的调用方式其实有 2 种,既可以采用类对象调用,也可以直接通过类名调用。

通常情况下,我们习惯使用类对象调用类中的实例方法。但如果想用类调用实例方法,不能像如下这样:

	class CLanguage:
	    def info(self):
	        print("我正在学 Python")
	#通过类名直接调用实例方法
	CLanguage.info()

运行上面代码,程序会报出如下错误:

Traceback (most recent call last):
File “D:\python3.6\demo.py”, line 5, in
CLanguage.info()
TypeError: info() missing 1 required positional argument: ‘self’

其中,最后一行报错信息提示我们,调用 info() 类方式时缺少给 self 参数传参。这意味着,和使用类对象调用实例方法不同,通过类名直接调用实例方法时,Python 并不会自动给 self 参数传值。
读者想想也应该明白,self 参数需要的是方法的实际调用者(是类对象),而这里只提供了类名,当然无法自动传值。

因此,如果想通过类名直接调用实例方法,就必须手动为 self 参数传值。例如修改上面的代码为:

	class CLanguage:
	    def info(self):
	        print("我正在学 Python")
	clang = CLanguage()
	#通过类名直接调用实例方法
	CLanguage.info(clang)

再次运行程序,结果为:

我正在学 Python

可以看到,通过手动将 clang 这个类对象传给了 self 参数,使得程序得以正确执行。实际上,这里调用实例方法的形式完全是等价于 clang.info()。

值得一提的是,上面的报错信息只是让我们手动为 self 参数传值,但并没有规定必须传一个该类的对象,其实完全可以任意传入一个参数,例如:

	class CLanguage:
	    def info(self):
	        print(self,"正在学 Python")
	#通过类名直接调用实例方法
	CLanguage.info("zhangsan")

运行结果为:

zhangsan 正在学 Python

可以看到,“zhangsan” 这个字符串传给了 info() 方法的 self 参数。显然,无论是 info() 方法中使用 self 参数调用其它类方法,还是使用 self 参数定义新的实例变量,胡乱的给 self 参数传参都将会导致程序运行崩溃。

总的来说,Python 中允许使用类名直接调用实例方法,但必须手动为该方法的第一个 self 参数传递参数,这种调用方法的方式被称为非绑定方法
用类的实例对象访问类成员的方式称为绑定方法,而用类名调用类成员的方式称为非绑定方法。

描述符

Python 中,通过使用描述符,可以让程序员在引用一个对象属性时自定义要完成的工作。

本质上看,描述符就是一个类,只不过它定义了另一个类中属性的访问方式。换句话说,一个类可以将属性管理全权委托给描述符类。

描述符是 Python 中复杂属性访问的基础,它在内部被用于实现 property、方法、类方法、静态方法和 super 类型。

描述符类基于以下 3 个特殊方法,换句话说,这 3 个方法组成了描述符协议

__set__(self, obj, type=None):在设置属性时将调用这一方法(本节后续用 setter 表示);
__get__(self, obj, value):在读取属性时将调用这一方法(本节后续用 getter 表示);
__delete__(self, obj):对属性调用 del 时将调用这一方法。
其中,实现了 setter 和 getter 方法的描述符类被称为数据描述符;反之,如果只实现了 getter 方法,则称为非数据描述符。

为了表达清楚,这里举个例子:

	#描述符类
	class revealAccess:
	    def __init__(self, initval = None, name = 'var'):
	        self.val = initval
	        self.name = name
	    def __get__(self, obj, objtype):
	        print("Retrieving",self.name)
	        return self.val
	    def __set__(self, obj, val):
	        print("updating",self.name)
	        self.val = val
	class myClass:
	    x = revealAccess(10,'var "x"')
	    y = 5
	m = myClass()
	print(m.x)
	m.x = 20
	print(m.x)
	print(m.y)

运行结果为:

Retrieving var “x”
10
updating var “x”
Retrieving var “x”
20
5

从这个例子可以看到,如果一个类的某个属性有数据描述符,那么每次查找这个属性时,都会调用描述符的 __get__() 方法,并返回它的值;同样,每次在对该属性赋值时,也会调用 __set__() 方法。

注意,虽然上面例子中没有使用 __del__() 方法,但也很容易理解,当每次使用 del 类对象.属性 语句时,都会调用该方法。

除了使用描述符类自定义类属性被调用时做的操作外,还可以使用 property() 函数或者 @property 装饰器,这些基本可以看作是描述符的简化,请大家自己去了解学习。

再谈Python封装机制

前面讲的都是python实现封装的一些手段。现在我们回过头来再看python的封装机制。

不光是 Python,大多数面向对象编程语言(诸如 C++、Java 等)都具备 3 个典型特征,即封装、继承和多态。

简单的理解封装(Encapsulation),即在设计类时,刻意地将一些属性和方法隐藏在类的内部,这样在使用此类时,将无法直接以“类对象.属性名”(或者“类对象.方法名(参数)”)的形式调用这些属性(或方法),而只能用未隐藏的类方法间接操作这些隐藏的属性和方法。

就好比使用电脑,我们只需要学会如何使用键盘和鼠标就可以了,不用关心内部是怎么实现的,因为那是生产和设计人员该操心的。
注意,封装绝不是将类中所有的方法都隐藏起来,一定要留一些像键盘、鼠标这样可供外界使用的类方法。

那么,类为什么要进行封装,这样做有什么好处呢?

首先,封装机制保证了类内部数据结构的完整性,因为使用类的用户无法直接看到类中的数据结构,只能使用类允许公开的数据,很好地避免了外部对内部数据的影响,提高了程序的可维护性。

除此之外,对一个类实现良好的封装,用户只能借助暴露出来的类方法来访问数据,我们只需要在这些暴露的方法中加入适当的控制逻辑,即可轻松实现用户对类中属性或方法的不合理操作。

并且,对类进行良好的封装,还可以提高代码的复用性。

Python 类如何进行封装?
和其它面向对象的编程语言(如 C++、Java)不同,Python 类中的变量和函数,不是公有的(类似 public 属性),就是私有的(类似 private),这 2 种属性的区别如下:

public:公有属性的类变量和类函数,在类的外部、类内部以及子类(后续讲继承特性时会做详细介绍)中,都可以正常访问;
private:私有属性的类变量和类函数,只能在本类内部使用,类的外部以及子类都无法使用。

但是,Python 并没有提供 public、private 这些修饰符。为了实现类的封装,Python 采取了下面的方法:
默认情况下,Python 类中的变量和方法都是公有(public)的,它们的名称前都没有下划线(_);
如果类中的变量和函数,其名称以双下划线“__”开头,则该变量(函数)为私有变量(私有函数),其属性等同于 private。

除此之外,还可以定义以单下划线“_”开头的类属性或者类方法(例如 _name、_display(self)),这种类属性和类方法通常被视为私有属性和私有方法,虽然它们也能通过类对象正常访问,但这是一种约定俗称的用法,初学者一定要遵守。

注意,Python 类中还有以双下划线开头和结尾的类方法(例如类的构造函数__init__(self)),这些都是 Python 内部定义的,用于 Python 内部调用。我们自己定义类属性或者类方法时,不要使用这种格式。

例如,如下程序示范了 Python 的封装机制:

	class CLanguage :
	    def setname(self, name):
	        if len(name) < 3:
	            raise ValueError('名称长度必须大于3!')
	        self.__name = name
	    def getname(self):
	        return self.__name
	    #为 name 配置 setter 和 getter 方法
	    name = property(getname, setname)
	    def setadd(self, add):
	        if add.startswith("http://"):
	            self.__add = add
	        else:
	            raise ValueError('地址必须以 http:// 开头') 
	    def getadd(self):
	        return self.__add
	   
	    #为 add 配置 setter 和 getter 方法
	    add = property(getadd, setadd)
	    #定义个私有方法
	    def __display(self):
	        print(self.__name,self.__add)
	clang = CLanguage()
	clang.name = "百度"
	clang.add = "http://www.baidu.com"
	print(clang.name)
	print(clang.add)

程序运行结果为:
百度
http://www.baidu.com

上面程序中,CLanguage 将 name 和 add 属性都隐藏了起来,但同时也提供了可操作它们的“窗口”,也就是各自的 setter 和 getter 方法,这些方法都是公有(public)的。

不仅如此,以 add 属性的 setadd() 方法为例,通过在该方法内部添加控制逻辑,即通过调用 startswith() 方法,控制用户输入的地址必须以“http://”开头,否则程序将会执行 raise 语句抛出 ValueError 异常。

这里 raise 的用法可简单理解成,如果用户输入不规范,程序将会报错。

通过此程序的运行逻辑不难看出,通过对 CLanguage 类进行良好的封装,使得用户仅能通过暴露的 setter() 和 getter() 方法操作 name 和 add 属性,而通过对 setname() 和 setadd() 方法进行适当的设计,可以避免用户对类中属性的不合理操作,从而提高了类的可维护性和安全性。

CLanguage 类中还有一个 __display() 方法,由于该类方法为私有(private)方法,且该类没有提供操作该私有方法的“窗口”,因此我们无法在类的外部使用它。换句话说,如下调用 __display() 方法是不可行的:

	#尝试调用私有的 display() 方法
	clang.__display()

这会导致如下错误:

Traceback (most recent call last):
File “D:\python3.6\1.py”, line 33, in
clang.__display()
AttributeError: ‘CLanguage’ object has no attribute ‘__display’

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值