练习40--模块、类和对象——基本概念
一 模块概念
1 模块就像字典:
- python中有一个非常通用的模式:
- 1 用一个键=值(key = value)形式的容器
- 2 通过键的名称从中获取内容
- 在字典中,键是一个字符串,语法是: [key] 。而在模块中,键是一个识别符,语法是 .key ,
- 除此之外它们几乎是同一种东西。
2 模块
- 定义:Python 模块(Module),是一个 Python 文件,以 .py 结尾,包含了 Python 对象定义和Python语句。
- 模块,可以理解为是对代码更高级的封装,即把能够实现某一特定功能的代码编写在同一个 .py 文件中,并将其作为一个独立的模块,这样既可以方便其它程序或脚本导入并使用,同时还能有效避免函数名和变量名发生冲突。
- 功能:
- 模块让你能够有逻辑地组织你的 Python 代码段。
- 把相关的代码分配到一个模块里能让你的代码更好用,更易懂。
- 模块能定义函数,类和变量,模块里也能包含可执行的代码。
- 创建:将所需代码保存在文件扩展名为 .py 的文件中的过程即为创建模块的过程
- 导入:我们可以通过 import 语句来导入我们创建的模块
- import 模块名1 [as 别名1], 模块名2 [as 别名2],…
- 特点:
- 使用这种语法格式的 import 语句,会导入指定模块中的所有成员(包括变量、函数、类等)
- 当需要使用模块中的成员时,需用该模块名(或别名)作为前缀,否则 Python 解释器会报错。
- 常见格式:
- import 模块名:最简单的语法,导入整个模块
- import 模块名 as 别名:导入整个模块时,也可以为模块指定别名。
- import 模块名1,模块名2:一次导入多个模块,多个模块之间用逗号隔开。
- import 模块名1 as 别名1,模块名2 as 别名2:在导入多个模块的同时,也可以为模块指定别名
- 特点:
- from import 语句:from 模块名 import 成员名1 [as 别名1],成员名2 [as 别名2],…
- 特点:
- 使用这种语法格式的 import 语句,只会导入模块中指定的成员,而不是全部成员。
- 当程序中使用该成员时,无需附加任何前缀,直接使用成员名(或别名)即可。
- 常见格式:
- from 模块名 import 成员名:最简单的语法,导入指定成员
- from 模块名 import 成员名 as 别名:导入模块成员时,也可以为成员指定别名
- from 模块名 import 成员名1,成员名2:from...import 导入模块成员时,支持一次导入多个成员
- from 模块名 import 成员名1 as 别名1,成员名2 as 别名2:一次导入多个模块成员时,也可指定别名,同样使用 as 关键字为成员指定别名
- from 模块名 import *:一般不推荐使用“from 模块 import *”这种语法导入指定模块内的所有成员,因为它存在潜在的风险。当两个不同模块中出现同名函数时,调用该函数会出现问题
- 特点:
- reload()函数:
- 功能:该函数会重新导入之前导入过的模块
- 语法:reload(module_name)
- 应用场景:当一个模块被导入到一个脚本,模块顶层部分的代码只会被执行一次。如果想重新执行模块里顶层部分的代码,可以用 reload() 函数
- import 模块名1 [as 别名1], 模块名2 [as 别名2],…
- 环境变量PYTHONPATH的相关内容:
- PYTHONPATH变量:环境变量PYTHONPATH 由装在一个列表里的许多目录组成。
- 模块搜索路径:当你导入一个模块,Python 解析器对模块位置的搜索顺序是:
- 1、当前目录
- 2、如果不在当前目录,Python 则搜索在 shell 变量 PYTHONPATH 下的每个目录。
- 3、如果都找不到,Python会察看默认路径。UNIX下,默认路径一般为/usr/local/lib/python/。
- 模块搜索路径存储在 system 模块的 sys.path 变量中。变量里包含当前目录,PYTHONPATH和由安装过程决定的默认目录。
- 解决“Python找不到指定模块”的方法有 3 种:
- 1 向 sys.path 中临时添加模块文件存储位置的完整路径;
- 2 将模块放在 sys.path 变量中已包含的模块加载路径中;
- 3 设置 path 系统环境变量。
- 查看成员:
-
dir()函数:查看某指定模块包含的全部成员(包括变量、函数和类)
-
- 这里所指的全部成员,不仅包含可供我们调用的模块成员,还包含所有名称以双下划线“__”开头和结尾的成员,而这些“特殊”命名的成员,是为了在本模块中使用的,并不希望被其它文件调用。
- 带参数:dir(模块名)表示查看该模块内的所有内容;不带参数:dir()表示查看当前模块内定义的函数名/变量名
-
- _all_变量:
- 内容:该变量的值是一个列表,存储的是模块中一些成员(变量、函数或者类)的名称
- 功能:限制其它文件能够导入的模块成员有哪些
- 适用情况:__all__ 变量仅限于在其它文件中以“from 模块名 import *”的方式引入。
- 只有以“from 模块名 import *”形式导入的模块,当该模块设有 __all__ 变量时,只能导入该变量指定的成员,未指定的成员是无法导入的。
- 以“import 模块名”的形式导入模块。通过该方式导入模块后,总可以通过模块名前缀来调用模块内的所有成员(除了以下划线开头命名的成员)。
- 以“from 模块名 import 成员”的形式直接导入指定成员。使用此方式导入的模块,__all__ 变量即便设置,也形同虚设。
- 查看成员:借助该变量也可以查看模块(包)内__all__变量中包含的成员。模块名.__all__
- __all__ 变量在查看指定模块成员时,它不会显示模块中的特殊成员,同时还会根据成员的名称进行排序显示。
- 并非所有的模块都支持使用 __all__ 变量,因此对于获取有些模块的成员(与它的适用情况有关),就只能使用 dir() 函数。
-
- __name__属性
- 功能:一个模块被另一个程序第一次引入时,其主程序将运行。如果我们想在模块被引入时,模块中的某一程序块不执行,我们可以用__name__属性来使该程序块仅在该模块自身运行时执行。
- 说明:
- 每个模块都有一个__name__属性,当其值是'__main__'时,表明该模块自身在运行,否则是被引入。
- __name__ 与 __main__ 底下是双下划线, _ _ 是这样去掉中间的那个空格。
- __doc__属性:查看文档
- 原因:
- 通过使用 dir() 函数和 __all__ 变量,我们能知晓指定模块(或包)中所有可用的成员(变量、函数和类)
- 通过查看帮助文档,我们能够弄清楚清楚这些名称的是什么意思,以及各个成员有什么功能。
- 方法:
- help()函数:获取指定成员(甚至是该模块)的帮助信息(该成员本身就包含表示自身身份的说明文档(本质是字符串,位于该成员内部开头的位置)的情况下)。
- __doc__属性:获取说明文档,函数、类、模块都可以。使用方法是:函数名/模块名/类名.__doc__
- help()函数和__doc__属性都不能满足要求:
- 调用 __file__ 属性,查看该模块或者包文件的具体存储位置,直接查看其源代码;
- 对于非自定义的模块或者包,可以查阅 Python 库的参考文档 https://docs.python.org/3/library/index.html。
- 原因:
- __file__属性:查看模块源文件路径
- 功能:查找该模块(或包)文件所在的具体存储位置(绝对路径),直接查看其源代码。使用方法:模块名.__file__
- 注意:并不是所有模块都提供 __file__ 属性,因为并不是所有模块的实现都采用 Python 语言,有些模块采用的是其它编程语言(如 C 语言)。
- 命名空间和作用域:https://www.runoob.com/python3/python3-namespace-scope.html
- 变量:指的是拥有匹配对象的名字(标识符)
- 命名空间:是一个包含了变量名称们(键)和它们各自相应的对象们(值)的字典。
- 局部命名空间:局部变量的作用域
- 全局命名空间:全局变量的作用域
- 特点:
- 一个 Python 表达式可以访问局部命名空间和全局命名空间里的变量。
- 如果一个局部变量和一个全局变量重名,则局部变量会覆盖全局变量。
- Python 会智能地猜测一个变量是局部的还是全局的,它假设任何在函数内赋值的变量都是局部的。因此,如果要给函数内的全局变量赋值,必须使用 global 语句。
- globals()函数和locals()函数
- 根据调用地方的不同,globals() 和 locals() 函数可被用来返回全局和局部命名空间里的名字。
- 如果在函数内部调用 locals(),返回的是所有能在该函数里访问的命名。
- 如果在函数内部调用 globals(),返回的是所有在该函数里能访问的全局名字。
- 两个函数的返回类型都是字典。所以名字们能用 keys() 函数摘取。
二 类和对象的概念
1 面向对象引子及概念
- 面向过程:
- 定义:
- 按照业务逻辑和实现过程步骤来逐步垒代码,代码编写的逻辑即对应于实际实现的步骤过程,核心是过程两个字,从代码执行顺序上体现出设计者的逻辑过程,整个程序就是把若干个过程串起来的效果。本质上像是构建了一条生成流水线,每一道工序都通过代码块严格定义。
- 优点:
- 复杂问题简单化,把大的任务逐步分解成一个一个小的任务分步实现,实现了每个小的步骤即可完成整体任务。逻辑思想符合日常生活中的常规过程化思维,因而代码可读性较高。
- 缺点:
- 由于实现逻辑是自上而下的分步过程,任何一个环节的变动,都可能涉及到后续环节的变动,牵一发而动全身。因此代码的灵活性、可维护性较差:构建的生产面包的流水线,如果原料工艺发生变化,很可能要进行生产线的改造;当然想用它来生产饮料,几乎要推倒重来。
- 应用场景:
- 需求相对固化,不会经常变更,容易通过过程化思维顺序体现出来的情况。
- 定义:
- 函数式编程:
- 定义:
- 函数式编程也是一种面向过程的编程范式,主要变化在于把重要的功能逻辑通过函数进行封装,以便重复调用,同时也提高了代码的维护性。
- 缺点:
- 对于较为复杂的应用场景,遇到多个函数需要传递共同的参数时,代码量较大且比较繁杂。
- 定义:
- 面向对象:
- 定义:
- 面向对象是一个抽象归类的思维过程,先梳理出具有共同属性的个体对象,然后对他们抽象归类。这样整个编程的设计思想不再是面向过程中的自上而下的串联顺序关系,而变成一种属于和不属于,具有或者不具有(某种属性)的关系。
- 优点:
- 对于复杂的场景,功能实现的灵活性更好,换言之适配性和扩展性更好。
- 缺点:
- 面向对象是一个抽象的过程,不同于面向对象的流水线式顺序执行过程,编程的复杂程度更高,首要前提是要梳理出对象之间的关系,归类出相应的属性,对于开发者而言要求更高,需具备一定的储备之后才能胜任一般场景的编程开发。
- 应用场景:
- 需求经常变化且相对复杂的软件,一般需求的变化都集中在用户层,互联网应用,游戏等都是面向对象的程序设计大显身手的好地方。
- 定义:
- 面向对象常用术语:
- 例子:
-
1 class tortoise: 2 bodyColor = "绿色" 3 footNum = 4 4 weight = 10 5 hasShell = True 6 7 #会爬 8 def crawl(self): 9 print("乌龟会爬") 10 #会吃东西 11 def eat(self): 12 print("乌龟吃东西") 13 #会睡觉 14 def sleep(self): 15 print("乌龟在睡觉") 16 #会缩到壳里 17 def protect(self): 18 print("乌龟缩进了壳里")
- 类:
- 可以理解是一个模板,通过它可以创建出无数个具体实例。比如,前面编写的 tortoise 表示的只是乌龟这个物种,通过它可以创建出无数个实例来代表各种不同特征的乌龟(这一过程又称为类的实例化)。
- 对象:
- 类并不能直接使用,通过类创建出的实例(又称对象)才能使用。这有点像汽车图纸和汽车的关系,图纸本身(类)并不能为人们使用,通过图纸创建出的一辆辆车(对象)才能使用。
- 属性:
- 类中的所有变量称为属性。例如,tortoise 这个类中,bodyColor、footNum、weight、hasShell 都是这个类拥有的属性。
- 方法:
- 类中的所有函数通常称为方法。不过,和函数所有不同的是,类方法至少要包含一个 self 参数(后续会做详细介绍)。例如,tortoise 类中,crawl()、eat()、sleep()、protect() 都是这个类所拥有的方法,类方法无法单独使用,只能和类的对象一起使用。
2 类和对象的基本概念
- 概念1:
- 类更像是一个模板,是对众多具备相似特征(属性)和技能(方法/函数)的统一集中实现,略显抽象(不做实例化似乎感受不到它的存在)
- 对象是根据类这个模板创建的实例,是对具备某种特征(属性)和技能(方法/函数)的具体化个体式的实现(实例化的对象作为一个独立的个体存在)。有了对象这一具体的实例,才能让类中描述的特征(属性)和技能(方法/函数)能真正落地体现出来,否则类只是构想中的空中楼阁。
- 概念2:
- 类就像模块,你可以把模块想象成一个可以储存 Python 代码并且可以用 . 运算符获取它的特定字典。类是一种整合一组函数和数据的方式,它将函数和数据放在一个容器内以便你能通过 . 运算符运算符进行行访问。
- 对象就像导入(import),如果说类是一个小型模块,那么应该要有一个概念和 import 类似。这个概念就叫做“实例化”(instantiate),你可以理解为它是“创造”一词的华而不实、令人讨厌、自以为是的说法。当你实例化一个类,你得到的东西就叫做对象。你通过像函数一样调用这个类来实例化(创造)一个对象
- 概念3:
- 类就像创建新的小型模块的蓝本或定义。
- 实例化是你如何创建这些小型模块并同时导入它们。“实例化”的意思就是从类那里创建一个对象。
- 你所创建的小型模块的结果被称作对象,然后你把它赋给一个变量来使用。
3 类和对象的关系
- 在面向对象的编程范式下,我们需要通过类和对象来实现编程设计,因此面向对象是对类和对象的应用。
- 类即类别、种类,是面向对象设计最重要的概念,对象是特征与技能的结合体,而类则是一系列对象相似的特征与技能的结合体
- 类和对象产生的先后关系:
- 在现实世界中:先有对象,再有类
- 在程序中:务必保证先定义类,后产生对象
- “类提供默认行为,是实例的工厂”,我觉得这句原话非常经典,一下道破了类和实例的关系。看上面代码,体会一下,是不是这个理?所谓工厂,就是可以用同一个模子做出很多具体的产品。类就是那个模子,实例就是具体的产品。所以,实例是程序处理的实际对象。
- 类是由一些语句组成,但是实例,是通过调用类生成,每次调用一个类,就得到这个类的新的实例。
三 封装
面向对象编程语言的三个典型特征:封装、继承和多态。这里因为前面模块的内容涉及到了封装的概念,就简单了解一下。
1 封装的基本概念
- 不同定义——不管什么形式,封装的都是数据和方法,无论哪种层面的封装,都要对外界提供好访问你内部隐藏内容的接口。
- 代码封装,其实就是隐藏实现功能的具体代码,仅留给用户使用的接口,就好像使用计算机,用户只需要使用键盘、鼠标就可以实现一些功能,而根本不需要知道其内部是如何工作的
- 类的封装,即在设计类时,刻意地将一些属性和方法隐藏在类的内部,这样在使用此类时,将无法直接以“类对象.属性名”(或者“类对象.方法名(参数)”)的形式调用这些属性(或方法),而只能用未隐藏的类方法间接操作这些隐藏的属性和方法。
- 模块,可以理解为是对代码更高级的封装,即把能够实现某一特定功能的代码编写在同一个 .py 文件中,并将其作为一个独立的模块,这样既可以方便其它程序或脚本导入并使用,同时还能有效避免函数名和变量名发生冲突。
- 一般意义上的封装:我们一般讲的封装都是指类的封装
- 注意:封装绝不是将类中所有的方法都隐藏起来,一定要留一些像键盘、鼠标这样可供外界使用的类方法。
- 原因:封装不是单纯意义的隐藏
- 封装数据的主要原因是:保护隐私
- 封装方法的主要原因是:隔离复杂度
- 两个层面:
- 第一个层面的封装(什么都不用做):创建类和对象会分别创建二者的名称空间,我们只能用类名.或者obj.的方式去访问里面的名字,这本身就是一种封装。
- 注意:对于这一层面的封装(隐藏),类名.和实例名.就是访问隐藏属性的接口
- 第二个层面的封装:类中把某些属性和方法隐藏起来(或者说定义成私有的),只在类的内部使用、外部无法访问,或者留下少量接口(函数)供外部访问。
- 第一个层面的封装(什么都不用做):创建类和对象会分别创建二者的名称空间,我们只能用类名.或者obj.的方式去访问里面的名字,这本身就是一种封装。
2 类封装的好处
- 可维护性:封装机制保证了类内部数据结构的完整性,因为使用类的用户无法直接看到类中的数据结构,只能使用类允许公开的数据,很好地避免了外部对内部数据的影响,提高了程序的可维护性。
- 复杂度:对一个类实现良好的封装,用户只能借助暴露出来的类方法来访问数据,我们只需要在这些暴露的方法中加入适当的控制逻辑,即可轻松实现用户对类中属性或方法的不合理操作。
- 代码可移植性:对类进行良好的封装,还可以提高代码的复用性。
3 具有封装特性的结构:
- 诸多容器,例如列表、元组、字符串、字典等,它们都是对数据的封装;
- 函数是对 Python 代码的封装;
- 类是对方法和属性的封装,也可以说是对函数和数据的封装。
- 模块是对代码更高级的封装,将代码封装成一个文件,方便其它程序或脚本导入使用。
4 python类如何进行封装
- 原理:Python 类中的变量和函数,不是公有的(类似 public 属性),就是私有的(类似 private),因此可通过设置类中变量和函数的该属性来实现类的封装
- 区别:
- public:公有属性的类变量和类函数,在类的外部、类内部以及子类(后续讲继承特性时会做详细介绍)中,都可以正常访问;
- private:私有属性的类变量和类函数,只能在本类内部使用,类的外部以及子类都无法使用。
- 方法:
- 名称前没有下划线:默认这些python类中的变量和方法都是公有(public)的,可以通过类对象正常访问;
- 名称以双下划线“__”开头:表示这些python类中的变量和方法都是私有(private)的,只能在类内部使用,在类外部不能通过类对象进行访问;
- 类中所有双下划线开头的名称如__x都会自动变形成:_类名__x的形式:
- 类中定义的__x只能在内部使用,如self.__x,引用的就是变形的结果。
- 这种变形其实正是针对外部的变形,在外部是无法通过__x这个名字访问到的。
- 在子类定义的__x不会覆盖在父类定义的__x,因为子类中变形成了:_子类名__x,而父类中变形成了:_父类名__x,即双下滑线开头的属性在继承给子类时,子类是无法覆盖的。
- 注意:对于这一层面的封装(隐藏),我们需要在类中定义一个函数(接口函数)在它内部访问被隐藏的属性,然后外部就可以使用了。
- 这种变形需要注意的问题是:
- 这种机制也并没有真正意义上限制我们从外部直接访问属性,知道了类名和属性名就可以拼出名字:_类名__属性,然后就可以访问了,如a._A__N
- 变形的过程只在类的定义是发生一次,在定义后的赋值操作,不会变形
- 类中所有双下划线开头的名称如__x都会自动变形成:_类名__x的形式:
- 名称以单下划线“_”开头的类属性和类方法:通常将他们看作私有(private)的,但是它们可以通过类对象正常访问;
- 名称前后分别以双下划线开头和结尾:这些类方法都是python内部定义的,用于python内部调用,我们自己定义类属性或者类方法时,禁止使用这种形式。
参考内容:
菜鸟教程:https://www.runoob.com/python/python-modules.html
博客园:https://www.cnblogs.com/linupython/p/9388610.html
一个教程网站http://c.biancheng.net/view/4512.html
博客园:https://www.cnblogs.com/fenglinglf/p/7123052.html
四 代码
ex40.py
1 class Song(object): # 定义一个类 2 3 def __init__(self,lyrics): # 构造函数,类的初始化 4 self.lyrics = lyrics 5 6 def sing_me_a_song(self): # 定义一个函数 7 for line in self.lyrics: 8 print(line) 9 10 happy_bday = Song(["Happy birthday to you", 11 "I don't want to get sued", 12 "So I'll stop right there"]) # 实例化过程,得到对象happy_bday 13 14 bulls_on_parade = Song(["They rally around tha family", 15 "With pockets full of shells"])# 实例化过程,得到对象bulls_on_parade 16 17 happy_bday.sing_me_a_song() # 通过对象happy_bday调用类Song中的sing_me_a_song函数,打印参数内容 18 19 bulls_on_parade.sing_me_a_song() # 通过对象bulls_on_parade调用类Song中sing_me_a_song的函数,打印参数内容
执行结果:
1 PS E:\6_AI-Study\1_Python\2_code_python\02_LearnPythonTheHardWay> python ex40.py 2 Happy birthday to you 3 I don't want to get sued 4 So I'll stop right there 5 They rally around tha family 6 With pockets full of shells 7 PS E:\6_AI-Study\1_Python\2_code_python\02_LearnPythonTheHardWay>
更多关于类和对象的内容,可查看: