精华内容
下载资源
问答
  • python编程从入门到实践
    万次阅读 多人点赞
    更多相关内容
  • 课后所有章节参考答案代码
  • 课后习题答案以及代码
  • Python编程从入门到实践书本源代码文件.rar
  • python编程从入门到实践书中的Django项目未部署云服务器的版本,可在本地运行查看网页效果,包含用户注册登录功能,主题和条目数据录入等,具体可参考我的博文《python编程从入门到实践笔记》
  • 第三章 列表 3.1 什么是列表 列表由一系列按特定顺序排列的元素组成。 其中的元素之间可以没有任何关系 给列表指定一个表示复数的名称(如letters、digits或names)是个不错的主意。...位置0开始
  • Python编程从入门到实践第9章源码.zip
  • 本书旨在让你成为优秀的程序员,具体地说,是优秀的 Python 程序员。通过阅读本书,你将迅速掌握编程概念,打下坚实的基础,并养成良好的习惯。...学习 Python 高级技术,并能够更轻松地掌握其他编程语言。
  • 最近在学习python编程从入门到实践第一版,发现因为Django已经变成2.0版本,很多代码跑不通,走了很多弯路,在网上找到最新的第二版书中的代码,分享给大家
  • Python编程从入门到实践的相关学校资料
  • 适合作为学习python的第一本书,作为一个入门python教材,可以说这个通俗易懂了,这本书用了python3进行编程的,不同于python核心编程那本书用第二版,这本书上手快
  • 基础学习交流,Python入门
  • Python编程从入门到实践》源代码文件
  • python编程从入门到实践

    千次阅读 2020-06-17 14:20:54
    什么是编程语言?为何要有编程语言? 语言其实就是人与人之间沟通的介质,如英语,汉语,俄语等。 编程语言则是人与计算机之间沟通的介质, 编程的目的就是为了让计算机按照人类的思维逻辑(程序)自发地去工作从而把...

    1、计算机核心基础

    1.1 什么是语言?什么是编程语言?为何要有编程语言?

    语言其实就是人与人之间沟通的介质,如英语,汉语,俄语等。
    编程语言则是人与计算机之间沟通的介质,
    编程的目的就是为了让计算机按照人类的思维逻辑(程序)自发地去工作从而把人力解放出来
    

    二 计算机组成原理

    2.1、什么是计算机?

    俗称电脑,即通电的大脑,电脑二字蕴含了人类对计算机的终极期望,希望它能真的像人脑一样去工作,从而解放人力。
    

    2.2、为什么要用计算机?

    世界是由聪明的懒人统治的,任何时期,总有一群聪明的懒人想要奴隶别人。在奴隶制社会,聪明的懒人奴役的是真正的人,而人是无法不吃、不喝、不睡觉一直工作的,但是计算机作为一台机器是可以做到的,所以把计算机当奴隶是上上之选
    

    2.3、计算机的五大组成部分

    1.控制器
    	:控制器是计算机的指挥系统,用来控制计算机其他组件的运行,相当于人类的大脑
    2.运算器
    	:运算器是计算机的运算功能,用来做算术运算和逻辑运算,相当于人脑
    ps:控制器+运算器=CPU,cpu相当于人的大脑
    
    3.存储器
    	:存储器是计算机的记忆功能,用来存取数据。
    
    存储器主要分为内存与外存:
    
    ​ 内存相当于人的短期记忆。断电数据丢失
    
    ​ 外存(如磁盘),相当于记事的本子,断电数据不会丢失,是用来永久保存数据的
    
    ​ ps:内存的存取速度要远远高于外存
    4.输入设备input
    	:输入设备是计算接收外界输入数据的工具,如键盘、鼠标,相当于人的眼睛或耳朵。
    5.输出设备output
    	:输出设备是计算机向外输出数据的工具,如显示器、打印机,相当于人说的话,写出的文章。
    
    ps:存储器如内存、磁盘等既是输入设备又是输出设备,统称为IO设备
    

    三大核心硬件为CPU、内存、硬盘。

    程序最先是存放于硬盘中的,程序的运行是先从硬盘把代码加载到内存中,然后cpu是从内存中读取指令运行。

    三 操作系统概述

    3.1、操作系统的由来

    操作系统的功能就是帮我们把复杂的硬件的控制封装成简单的接口,对于开发应用程序来说只需要调用操作系统提供给我们的接口即可
    

    3.2、系统软件与应用软件

    硬件以上运行的都是软件,而软件分为两类:

    一、应用软件(例如qq、word、暴风影音,我们学习python就是为了开发应用软件的)
    
    二、操作系统,操作系统应用软件与硬件之间的一个桥梁,是协调、管理、控制计算机硬件与应用软件资源的控制程序。
    

    3.3、计算机系统三层结构

    应用程序
    操作系统
    计算机硬件
    

    硬件 + 操作系统 == 平台

    2、编程语言与Python介绍

    编程语言分类:

    机器语言

    机器语言是站在计算机(奴隶)的角度,说计算机能听懂/理解的语言,而计算机能直接理解的就是二进制指令,所以机器语言就是直接用二进制编程,这意味着机器语言是直接操作硬件的,因此机器语言属于低级语言,此处的低级指的是底层、贴近计算机硬件

    #机器语言
        用二进制代码0和1描述的指令称为机器指令,由于计算机内部是基于二进制指令工作的,所以机器语言是直接控制计算机硬件。
    
        用机器语言编写程序,编程人员要首先熟记所用计算机的全部指令代码以及代码的含义,然后在编写程序时,程序员得自己处理每条指令和每一数据的存储分配和输入输出,还得记住编程过程中每步所使用的工作单元处在何种状态。这是一件十分繁琐的工作。编写程序花费的时间往往是实际运行时间的几十倍或几百倍。而且,编出的程序全是些0和1的指令代码,直观性差,不便阅读和书写,还容易出错,且依赖于具体的计算机硬件型号,局限性很大。除了计算机生产厂家的专业人员外,绝大多数的程序员已经不再去学习机器语言了。
    
        机器语言是被微处理器理解和使用的,存在有多至100000种机器语言的指令,下述是一些简单示例
    
        #指令部份的示例
        0000 代表 加载(LOAD)
        0001 代表 存储(STORE)
        ...
    
        #暂存器部份的示例
        0000 代表暂存器 A
        0001 代表暂存器 B
        ...
    
        #存储器部份的示例
        000000000000 代表地址为 0 的存储器
        000000000001 代表地址为 1 的存储器
        000000010000 代表地址为 16 的存储器
        100000000000 代表地址为 2^11 的存储器
    
        #集成示例
        0000,0000,000000010000 代表 LOAD A, 16
        0000,0001,000000000001 代表 LOAD B, 1
        0001,0001,000000010000 代表 STORE B, 16
        0001,0001,000000000001 代表 STORE B, 1[1]
    

    总结机器语言

    # 1、执行效率最高
    编写的程序可以被计算机无障碍理解、直接运行,执行效率高 。
    
    # 2、开发效率最低
    复杂,开发效率低
    
    # 3、跨平台性差
    贴近\依赖具体的硬件,跨平台性差
    

    汇编语言

    汇编语言仅仅是用一个英文标签代表一组二进制指令,毫无疑问,比起机器语言,汇编语言是一种进步,但汇编语言的本质仍然是直接操作硬件,因此汇编语言仍是比较低级/底层的语言、贴近计算机硬件

    #汇编语言
    汇编语言的实质和机器语言是相同的,都是直接对硬件操作,只不过指令采用了英文缩写的标识符,更容易识别和记忆。它同样需要编程者将每一步具体的操作用命令的形式写出来。汇编程序的每一句指令只能对应实际操作过程中的一个很细微的动作。例如移动、自增,因此汇编源程序一般比较冗长、复杂、容易出错,而且使用汇编语言编程需要有更多的计算机专业知识,但汇编语言的优点也是显而易见的,用汇编语言所能完成的操作不是一般高级语言所能够实现的,而且源程序经汇编生成的可执行文件不仅比较小,而且执行速度很快。
    
    汇编的hello world,打印一句hello world, 需要写十多行,如下
    
    ; hello.asm 
    section .data            ; 数据段声明
            msg db "Hello, world!", 0xA     ; 要输出的字符串
            len equ $ - msg                 ; 字串长度
            section .text            ; 代码段声明
            global _start            ; 指定入口函数
            _start:                  ; 在屏幕上显示一个字符串
            mov edx, len     ; 参数三:字符串长度
            mov ecx, msg     ; 参数二:要显示的字符串
            mov ebx, 1       ; 参数一:文件描述符(stdout) 
            mov eax, 4       ; 系统调用号(sys_write) 
            int 0x80         ; 调用内核功能
                             ; 退出程序
            mov ebx, 0       ; 参数一:退出代码
            mov eax, 1       ; 系统调用号(sys_exit) 
            int 0x80         ; 调用内核功能
    

    总结汇编语言

    # 1、执行效率高
    相对于机器语言,使用英文标签编写程序相对简单,执行效率高,但较之机器语言稍低,
    
    # 2、开发效率低:
    仍然是直接操作硬件,比起机器语言来说,复杂度稍低,但依旧居高不下,所以开发效率依旧较低
    
    # 3、跨平台性差
    同样依赖具体的硬件,跨平台性差
    

    高级语言

    高级语言是站在人(奴隶主)的角度,说人话,即用人类的字符去编写程序,而人类的字符是在向操作系统发送指令,而非直接操作硬件,所以高级语言是与操作系统打交道的,此处的高级指的是高层、开发者无需考虑硬件细节,因而开发效率可以得到极大的提升,但正因为高级语言离硬件较远,更贴近人类语言,人类可以理解,而计算机则需要通过翻译才能理解,所以执行效率会低于低级语言。

    按照翻译的方式的不同,高级语言又分为两种:

    编译型(如C语言):

    类似谷歌翻译,是把程序所有代码编译成计算机能识别的二进制指令,之后操作系统会拿着编译好的二进制指令直接操作硬件,详细如下

    # 1、执行效率高
    编译是指在应用源程序执行之前,就将程序源代码“翻译”成目标代码(即机器语言),
    因此其目标程序可以脱离其语言环境独立执行,使用比较方便,执行效率较高。
    
    # 2、开发效率低:
    应用程序一旦需要修改,必须先修改源代码,然后重新编译、生成新的目标文件才能执行,
    而在只有目标文件而没有源代码,修改会很不方便。所以开发效率低于解释型
    
    # 3、跨平台性差
    编译型代码是针对某一个平台翻译的,当前平台翻译的结果无法拿到不同的平台使用,针对不同的平台必须重新编译,即跨平台性差
    
    # 其他
    现在大多数的编程语言都是编译型的。
    编译程序将源程序翻译成目标程序后保存在另一个文件中,该目标程序可脱离编译程序直接在计算机上多次运行。
    大多数软件产品都是以目标程序形式发行给用户的,不仅便于直接运行,同时又使他人难于盗用其中的技术。
    C、C++、Ada、Pascal都是编译实现的
    

    解释型(如python):

    类似同声翻译,需要有一个解释器,解释器会读取程序代码,一边翻译一边执行,详细如下
    
    # 1、执行效率低
    解释型语言的实现中,翻译器并不产生目标机器代码,而是产生易于执行的中间代码。
    这种中间代码与机器代码是不同的,中间代码的解释是由软件支持的,不能直接使用硬件,
    软件解释器通常会导致执行效率较低。
    
    # 2、开发效率高
    用解释型语言编写的程序是由另一个可以理解中间代码的解释程序执行的,与编译程序不同的是,
    解释程序的任务是逐一将源程序的语句解释成可执行的机器指令,不需要将源程序翻译成目标代码再执行。
    解释程序的优点是当语句出现语法错误时,可以立即引起程序员的注意,而程序员在程序开发期间就能进行校正。
    
    
    # 3、跨平台性强
    代码运行是依赖于解释器,不同平台有对应版本的解释器,所以解释型的跨平台性强
    
    # 其他
    对于解释型Basic语言,需要一个专门的解释器解释执行Basic程序,每条语句只有在执行时才被翻译,
    这种解释型语言每执行一次就翻译一次,因而效率低下。一般地,动态语言都是解释型的,
    例如:Tcl、Perl、Ruby、VBScript、JavaScript等
    

    ps:混合型语言

    java是一类特殊的编程语言,Java程序也需要编译,但是却没有直接编译为机器语言,而是编译为字节码,
    然后在Java虚拟机上以解释方式执行字节码。
    

    总结

    综上选择不同编程语言来开发应用程序对比

    #1、执行效率:机器语言>汇编语言>高级语言(编译型>解释型)
    
    #2、开发效率:机器语言<汇编语言<高级语言(编译型<解释型)
    
    #3、跨平台性:解释型具有极强的跨平台型
    

    三 python介绍

    谈及python,涉及两层意思,一层代表的是python这门语言的语法风格,另外一层代表的则是专门用来解释该语法风格的应用程序:python解释器。

    Python崇尚优美、清晰、简单,是一个优秀并广泛使用的语言

    Python解释器的发展史

    从一出生,Python已经具有了:类,函数,异常处理,包含表和词典在内的核心数据类型,以及模块为基础的拓展系统。

    Python解释器有哪些种类?

    官方的Python解释器本质就是基于C语言开发的一个软件,该软件的功能就是读取以.py结尾的文件内容,然后按照Guido定义好的语法和规则去翻译并执行相应的代码。

    # Jython
    JPython解释器是用JAVA编写的python解释器,可以直接把Python代码编译成Java字节码并执行,它不但使基于java的项目之上嵌入python脚本成为可能,同时也可以将java程序引入到python程序之中。
    
    # IPython
    IPython是基于CPython之上的一个交互式解释器,也就是说,IPython只是在交互方式上有所增强,但是执行Python代码的功能和CPython是完全一样的。这就好比很多国产浏览器虽然外观不同,但内核其实都是调用了IE。
    CPython用>>>作为提示符,而IPython用In [序号]:作为提示符。
    
    # PyPy
    PyPy是Python开发者为了更好地Hack Python而用Python语言实现的Python解释器。PyPy提供了JIT编译器和沙盒功能,对Python代码进行动态编译(注意不是解释),因此运行速度比CPython还要快。
    
    # IronPython
    IronPython和Jython类似,只不过IronPython是运行在微软.Net平台上的Python解释器,可以直接把Python代码编译成.Net的字节码。
    

    四 安装Cpython解释器

    Python解释器目前已支持所有主流操作系统,在Linux,Unix,Mac系统上自带Python解释器,在Windows系统上需要安装一下,具体步骤如下。

    4.1、下载python解释器

    https://www.python.org

    img

    img

    img

    4.2、安装python解释器

    img

    img

    img

    4.3、测试安装是否成功

    windows --> 运行 --> 输入cmd ,然后回车,弹出cmd程序,输入python,如果能进入交互环境 ,代表安装成功。

    img

    img

    五 第一个python程序

    5.1 运行python程序有两种方式

    方式一: 交互式模式

    img

    方式二:脚本文件

    # 1、打开一个文本编辑工具,写入下述代码,并保存文件,此处文件的路径为D:\test.py。强调:python解释器执行程序是解释执行,解释的根本就是打开文件读内容,因此文件的后缀名没有硬性限制,但通常定义为.py结尾
    print('hello world')
    
    # 2、打开cmd,运行命令,如下图
    

    img

    总结:

    #1、交互式模式下可以即时得到代码执行结果,调试程序十分方便
    #2、若想将代码永久保存下来,则必须将代码写入文件中
    #3、我们以后主要就是在代码写入文件中,偶尔需要打开交互式模式调试某段代码、验证结果
    

    5.2 注释

    在正式学习python语法前,我们必须事先介绍一个非常重要的语法:注释

    1、什么是注释

    注释就是就是对代码的解释说明,注释的内容不会被当作代码运行
    

    2、为什么要注释

    增强代码的可读性
    

    3、怎么用注释?

    代码注释分单行和多行注释
    
    1、单行注释用#号,可以跟在代码的正上方或者正后方
    
    2、多行注释可以用三对双引号""" """
    

    4、代码注释的原则:

    1、不用全部加注释,只需要为自己觉得重要或不好理解的部分加注释即可
    
    2、注释可以用中文或英文,但不要用拼音
    

    六 IDE工具pycharm的使用

    在编写第一个python程序时,存在以下问题,严重影响开发效率

    问题一:我们了解到一个python程序从开发到运行需要操作至少两个软件

    1、打开一个软件:文本编辑器,创建文本来编写程序
    2、打开cmd,然后输入命令执行pyton程序
    

    问题二:在开发过程中,并没代码提示以及纠错功能

    综上,如果能有一款工具能够集成n个软件的功能,同时又代码提示以及纠错等功能,那么将会极大地提升程序员的开发效率,这就是IDE的由来,IDE全称Integrated Development Environment,即集成开发环境,最好的开发Python程序的IDE就是PyCharm。
    

    6.2、pychram安装

    # 下载地址: https://www.jetbrains.com/pycharm/download  选择Professional专业版
    

    6.3、Pycharm创建文件夹

    6.4、如何创建文件并编写程序执行

    创建py文件test.py

    在test.py中写代码,输入关键字的开头可以用tab键补全后续,并且会有代码的错误提示

    3、Python语法入门之变量

    一 引入

    我们学习python语言是为了控制计算机、让计算机能够像人一样去工作,所以在python这门语言中,所有语法存在的意义都是为了让计算机具备人的某一项技能,这句话是我们理解后续所有python语法的根本。

    二 变量

    一、什么是变量?

    # 变量就是可以变化的量,量指的是事物的状态,比如人的年龄、性别,游戏角色的等级、金钱等等
    

    二、为什么要有变量?

    # 为了让计算机能够像人一样去记忆事物的某种状态,并且状态是可以发生变化的
    # 详细地说:
    # 程序执行的本质就是一系列状态的变化,变是程序执行的直接体现,所以我们需要有一种机制能够反映或者说是保存下来程序执行时状态,以及状态的变化。
    

    三、怎么使用变量(先定义、后使用)

    3.1、变量的定义与使用

    img

    定义变量示范如下

    name = 'harry' # 记下人的名字为'harry'
    sex = '男'    # 记下人的性别为男性
    age = 18      # 记下人的年龄为18岁
    salary = 30000.1  # 记下人的薪资为30000.1元
    

    解释器执行到变量定义的代码时会申请内存空间存放变量值,然后将变量值的内存地址绑定给变量名,以变量的定义age=18为例,如下图

    插图:定义变量申请内存

    通过变量名即可引用到对应的值

    # 通过变量名即可引用到值,我们可以结合print()功能将其打印出来
    print(age) # 通过变量名age找到值18,然后执行print(18),输出:18
    
    # 命名规范
    1. 变量名只能是 字母、数字或下划线的任意组合
    2. 变量名的第一个字符不能是数字
    3. 关键字不能声明为变量名,常用关键字如下
    ['and', 'as', 'assert', 'break', 'class', 'continue', 'def', 'del', 'elif', 'else', 'except', 'exec', 'finally', 'for', 'from','global', 'if', 'import', 'in', 'is', 'lambda', 'not', 'or', 'pass', 'print', 'raise', 'return', 'try', 'while', 'with', 'yield']
    年龄=18 # 强烈建议不要使用中文命名
    
    

    3.3、变量名的命名风格

    # 风格一:驼峰体
    AgeOfTony = 56 
    NumberOfStudents = 80
    # 风格二:纯小写下划线(在python中,变量名的命名推荐使用该风格)
    age_of_tony = 56 
    number_of_students = 80
    

    3.4、变量值的三大特性

    #1、id
    反应的是变量在内存中的唯一编号,内存地址不同id肯定不同
    
    #2、type
    变量值的类型
    
    #3、value
    变量值
    

    三、常量

    3.1、什么是常量?

    常量指在程序运行过程中不会改变的量

    3.2、为什么要有常量?

    在程序运行过程中,有些值是固定的、不应该被改变,比如圆周率 3.141592653…

    3.3、怎么使用常量?

    在Python中没有一个专门的语法定义常量,约定俗成是用全部大写的变量名表示常量。如:PI=3.14159。所以单从语法层面去讲,常量的使用与变量完全一致。

    4、Python语法入门之基本数据类型

    一 引入

    变量值也有不同的类型

    salary = 3.1 # 用浮点型去记录薪资
    age = 18 # 用整型去记录年龄
    name = 'lili' # 用字符串类型去记录人名
    

    二 数字类型

    2.1 int整型

    2.1.1 作用

    用来记录人的年龄,出生年份,学生人数等整数相关的状态

    2.1.2 定义

    age=18
    
    birthday=1990
    
    student_count=48
    

    2.2 float浮点型

    2.2.1 作用

    用来记录人的身高,体重,薪资等小数相关的状态

    2.2.2 定义

    height=172.3
    
    weight=103.5
    
    salary=15000.89
    

    2.3 数字类型的使用

    1 、数学运算

    >>> a = 1
    >>> b = 3
    >>> c = a + b
    >>> c
    4
    

    2、比较大小

    >>> x = 10
    >>> y = 11
    >>> x > y
    False
    

    三 字符串类型str

    3.1 作用

    用来记录人的名字,家庭住址,性别等描述性质的状态

    3.2 定义

    name = 'harry'
    
    address = '上海市浦东新区'
    
    sex = '男'
    

    用单引号、双引号、多引号,都可以定义字符串,本质上是没有区别的,但是

    #1、需要考虑引号嵌套的配对问题
    msg = "My name is Tony , I'm 18 years old!" #内层有单引号,外层就需要用双引号
    #2、多引号可以写多行字符串
    msg = '''
            天下只有两种人。比如一串葡萄到手,一种人挑最好的先吃,另一种人把最好的留到最后吃。
            照例第一种人应该乐观,因为他每吃一颗都是吃剩的葡萄里最好的;第二种人应该悲观,因为他每吃一颗都是吃剩的葡萄里最坏的。
            不过事实却适得其反,缘故是第二种人还有希望,第一种人只有回忆。
          '''
    

    3.3 使用

    数字可以进行加减乘除等运算,字符串呢?也可以,但只能进行"相加"和"相乘"运算。
    >>> name = 'tony'
    >>> age = '18'
    >>> name + age #相加其实就是简单的字符串拼接
    'tony18'
    >>> name * 5 #相乘就相当于将字符串相加了5次
    'tonytonytonytonytony'
    

    四 列表list

    4.1 作用

    如果我们需要用一个变量记录多个学生的姓名,用数字类型是无法实现,字符串类型确实可以记录下来,比如

    stu_names=‘张三 李四 王五’,但存的目的是为了取,此时若想取出第二个学生的姓名实现起来相当麻烦,而列表类型就是专门用来记录多个同种属性的值(比如同一个班级多个学生的姓名、同一个人的多个爱好等),并且存取都十分方便

    4.2 定义

    >>> stu_names=['张三','李四','王五']
    

    4.3 使用

    # 1、列表类型是用索引来对应值,索引代表的是数据的位置,从0开始计数
    >>> stu_names=['张三','李四','王五']
    >>> stu_names[0] 
    '张三'
    >>> stu_names[1]
    '李四'
    >>> stu_names[2]
    '王五'
    # 2、列表可以嵌套,嵌套取值如下
    >>> students_info=[['tony',18,['jack',]],['jason',18,['play','sleep']]]
    >>> students_info[0][2][0] #取出第一个学生的第一个爱好
    'play'
    

    五 字典dict

    5.1 作用

    如果我们需要用一个变量记录多个值,但多个值是不同属性的,比如人的姓名、年龄、身高,用列表可以存,但列表是用索引对应值的,而索引不能明确地表示值的含义,这就用到字典类型,字典类型是用key:value形式来存储数据,其中key可以对value有描述性的功能

    5.2 定义

    >>> person_info={'name':'tony','age':18,'height':185.3}
    

    5.3 使用

    # 1、字典类型是用key来对应值,key可以对值有描述性的功能,通常为字符串类型
    >>> person_info={'name':'tony','age':18,'height':185.3}
    >>> person_info['name']
    'tony'
    >>> person_info['age']
    18
    >>> person_info['height']
    185.3
    # 2、字典可以嵌套,嵌套取值如下
    >>> students=[
    ... {'name':'tony','age':38,'hobbies':['play','sleep']},
    ... {'name':'jack','age':18,'hobbies':['read','sleep']},
    ... {'name':'rose','age':58,'hobbies':['music','read','sleep']},
    ... ]
    >>> students[1]['hobbies'][1] #取第二个学生的第二个爱好
    'sleep'
    

    六 布尔bool

    6.1 作用

    用来记录真假这两种状态

    6.2 定义

    >>> is_ok = True
    >>> is_ok = False
    

    6.3 使用

    通常用来当作判断的条件,我们将在if判断中用到它
    

    5、Python语法入门之垃圾回收机制

    一 引入

    解释器在执行到定义变量的语法时,会申请内存空间来存放变量的值,而内存的容量是有限的,这就涉及到变量值所占用内存空间的回收问题,当一个变量值没有用了(简称垃圾)就应该将其占用的内存给回收掉,那什么样的变量值是没有用的呢?
    
    ​ 单从逻辑层面分析,我们定义变量将变量值存起来的目的是为了以后取出来使用,而取得变量值需要通过其绑定的直接引用(如x=10,10被x直接引用)或间接引用(如l=[x,],x=10,10被x直接引用,而被容器类型l间接引用),所以当一个变量值不再绑定任何引用时,我们就无法再访问到该变量值了,该变量值自然就是没有用的,就应该被当成一个垃圾回收。
    
    ​ 毫无疑问,内存空间的申请与回收都是非常耗费精力的事情,而且存在很大的危险性,稍有不慎就有可能引发内存溢出问题,好在Cpython解释器提供了自动的垃圾回收机制来帮我们解决了这件事。
    

    二、什么是垃圾回收机制?

    垃圾回收机制(简称GC)是Python解释器自带一种机,专门用来回收不可用的变量值所占用的内存空间
    

    三、为什么要用垃圾回收机制?

    程序运行过程中会申请大量的内存空间,而对于一些无用的内存空间如果不及时清理的话会导致内存使用殆尽(内存溢出),导致程序崩溃,因此管理内存是一件重要且繁杂的事情,而python解释器自带的垃圾回收机制把程序员从繁杂的内存管理中解放出来。
    

    四、理解GC原理需要储备的知识

    4.1、堆区与栈区

    在定义变量时,变量名与变量值都是需要存储的,分别对应内存中的两块区域:堆区与栈区

    # 1、变量名与值内存地址的关联关系存放于栈区
    
    # 2、变量值存放于堆区,内存管理回收的则是堆区的内容,
    

    4.2 直接引用与间接引用

    直接引用指的是从栈区出发直接引用到的内存地址。

    间接引用指的是从栈区出发引用到堆区后,再通过进一步引用才能到达的内存地址。

    l2 = [20, 30]  # 列表本身被变量名l2直接引用,包含的元素被列表间接引用
    x = 10  # 值10被变量名x直接引用
    l1 = [x, l2]  # 列表本身被变量名l1直接引用,包含的元素被列表间接引用
    

    img

    五、垃圾回收机制原理分析

    Python的GC模块主要运用了“引用计数”(reference counting)来跟踪和回收垃圾。在引用计数的基础上,还可以通过“标记-清除”(mark and sweep)解决容器对象可能产生的循环引用的问题,并且通过“分代回收”(generation collection)以空间换取时间的方式来进一步提高垃圾回收的效率。

    5.1、引用计数

    引用计数就是:变量值被变量名关联的次数

    如:age=18

    变量值18被关联了一个变量名age,称之为引用计数为1

    img

    引用计数增加:

    age=18 (此时,变量值18的引用计数为1)

    m=age (把age的内存地址给了m,此时,m,age都关联了18,所以变量值18的引用计数为2)

    img

    引用计数减少:

    age=10(名字age先与值18解除关联,再与3建立了关联,变量值18的引用计数为1)

    del m(del的意思是解除变量名x与变量值18的关联关系,此时,变量18的引用计数为0)

    img

    值18的引用计数一旦变为0,其占用的内存地址就应该被解释器的垃圾回收机制回收

    5.2、引用计数的问题与解决方案

    5.2.1 问题一:循环引用

    引用计数机制存在着一个致命的弱点,即循环引用(也称交叉引用)

    # 如下我们定义了两个列表,简称列表1与列表2,变量名l1指向列表1,变量名l2指向列表2
    >>> l1=['xxx']  # 列表1被引用一次,列表1的引用计数变为1   
    >>> l2=['yyy']  # 列表2被引用一次,列表2的引用计数变为1   
    >>> l1.append(l2)             # 把列表2追加到l1中作为第二个元素,列表2的引用计数变为2
    >>> l2.append(l1)             # 把列表1追加到l2中作为第二个元素,列表1的引用计数变为2
    
    # l1与l2之间有相互引用
    # l1 = ['xxx'的内存地址,列表2的内存地址]
    # l2 = ['yyy'的内存地址,列表1的内存地址]
    >>> l1
    ['xxx', ['yyy', [...]]]
    >>> l2
    ['yyy', ['xxx', [...]]]
    >>> l1[1][1][0]
    'xxx'
    

    img

    python引入了“标记-清除” 与“分代回收”来分别解决引用计数的循环引用与效率低的问题

    5.2.2 解决方案:标记-清除

    容器对象(比如:list,set,dict,class,instance)都可以包含对其他对象的引用,所以都可能产生循环引用。而“标记-清除”计数就是为了解决循环引用的问题。

    标记/清除算法的做法是当应用程序可用的内存空间被耗尽的时,就会停止整个程序,然后进行两项工作,第一项则是标记,第二项则是清除

    5.2.3 问题二:效率问题

    基于引用计数的回收机制,每次回收内存,都需要把所有对象的引用计数都遍历一遍,这是非常消耗时间的,于是引入了分代回收来提高回收效率,分代回收采用的是用“空间换时间”的策略。
    

    5.2.4 解决方案:分代回收

    代:

    分代回收的核心思想是:在历经多次扫描的情况下,都没有被回收的变量,gc机制就会认为,该变量是常用变量,gc对其扫描的频率会降低,具体实现原理如下:

    分代指的是根据存活时间来为变量划分不同等级(也就是不同的代)
    
    新定义的变量,放到新生代这个等级中,假设每隔1分钟扫描新生代一次,如果发现变量依然被引用,那么该对象的权重(权重本质就是个整数)加一,当变量的权重大于某个设定得值(假设为3),会将它移动到更高一级的青春代,青春代的gc扫描的频率低于新生代(扫描时间间隔更长),假设5分钟扫描青春代一次,这样每次gc需要扫描的变量的总个数就变少了,节省了扫描的总时间,接下来,青春代中的对象,也会以同样的方式被移动到老年代中。也就是等级(代)越高,被垃圾回收机制扫描的频率越低
    

    回收:

    回收依然是使用引用计数作为回收的依据

    img

    虽然分代回收可以起到提升效率的效果,但也存在一定的缺点:

    例如一个变量刚刚从新生代移入青春代,该变量的绑定关系就解除了,该变量应该被回收,但青春代的扫描频率低于新生代,这就到导致了应该被回收的垃圾没有得到及时地清理。
    
    没有十全十美的方案:
    毫无疑问,如果没有分代回收,即引用计数机制一直不停地对所有变量进行全体扫描,可以更及时地清理掉垃圾占用的内存,但这种一直不停地对所有变量进行全体扫描的方式效率极低,所以我们只能将二者中和。
    
    综上
    垃圾回收机制是在清理垃圾&释放内存的大背景下,允许分代回收以极小部分垃圾不会被及时释放为代价,以此换取引用计数整体扫描频率的降低,从而提升其性能,这是一种以空间换时间的解决方案目录
    

    6、Python语法入门之与用户交互、运算符

    一 程序与用户交互

    1.1、什么是与用户交互

    用户交互就是人往计算机中input/输入数据,计算机print/输出结果

    1.2、为什么要与用户交互?

    为了让计算机能够像人一样与用户沟通交流
    

    1.3、如何与用户交互

    交互的本质就是输入、输出
    

    1.3.1 输入input:

    # 在python3中input功能会等待用户的输入,用户输入任何内容,都存成字符串类型,然后赋值给等号左边的变量名
    >>> username=input('请输入您的用户名:') 
    请输入您的用户名:jack # username = "jack"
    >>> password=input('请输入您的密码:') 
    请输入您的密码:123 # password = "123"
    
    # 了解知识:
    # 1、在python2中存在一个raw_input功能与python3中的input功能一模一样
    # 2、在python2中还存在一个input功能,需要用户输入一个明确的数据类型,输入什么类型就存成什么类型
    >>> l=input('输入什么类型就存成什么类型: ')
    输入什么类型就存成什么类型: [1,2,3]
    >>> type(l)
    <type 'list'>
    

    1.3.2 输出print:

    >>> print('hello world')  # 只输出一个值
    hello world
    >>> print('first','second','third')  # 一次性输出多个值,值用逗号隔开
    first second third
    
    # 默认print功能有一个end参数,该参数的默认值为"\n"(代表换行),可以将end参数的值改成任意其它字符
    print("aaaa",end='')
    print("bbbb",end='&')
    print("cccc",end='@')
    #整体输出结果为:aaaabbbb&cccc@
    

    1.3.3 输出之格式化输出

    (1)什么是格式化输出?

    把一段字符串里面的某些内容替换掉之后再输出,就是格式化输出。

    (2)为什么要格式化输出?

    我们经常会输出具有某种固定格式的内容,比如:'亲爱的xxx你好!你xxx月的话费是xxx,余额是xxx‘,我们需要做的就是将xxx替换为具体的内容。

    (3)如何格式化输出?

    这就用到了占位符,如:%s、%d:

    # %s占位符:可以接收任意类型的值
    # %d占位符:只能接收数字
    >>> print('亲爱的%s你好!你%s月的话费是%d,余额是%d' %('tony',12,103,11))
    亲爱的tony你好!你12月的话费是103,余额是11
    
    # 练习1:接收用户输入,打印成指定格式
    name = input('your name: ')
    age = input('your age: ') #用户输入18,会存成字符串18,无法传给%d
    print('My name is %s,my age is %s' %(name,age))
    
    # 练习2:用户输入姓名、年龄、工作、爱好 ,然后打印成以下格式
    ------------ info of Tony -----------
    Name  : Tony
    Age   : 22
    Sex   : male
    Job   : Teacher 
    ------------- end -----------------
    

    二 基本运算符

    2.1 算术运算符

    python支持的算数运算符与数学上计算的符号使用是一致的,我们以x=9,y=2为例来依次介绍它们

    img

    2.2 比较运算符

    比较运算用来对两个值进行比较,返回的是布尔值True或False,我们以x=9,y=2为例来依次介绍它们

    img

    2.3 赋值运算符

    python语法中除了有=号这种简单的赋值运算外,还支持增量赋值、链式赋值、交叉赋值、解压赋值,这些赋值运算符存在的意义都是为了让我们的代码看起来更加精简。我们以x=9,y=2为例先来介绍一下增量赋值

    2.3.1 增量赋值

    img

    2.3.2 链式赋值

    如果我们想把同一个值同时赋值给多个变量名,可以这么做

    >>> z=10
    >>> y=z
    >>> x=y
    >>> x,y,z
    (10, 10, 10)
    

    链式赋值指的是可以用一行代码搞定这件事

    >>> x=y=z=10
    >>> x,y,z
    (10, 10, 10)
    

    2.3.3 交叉赋值

    我们定义两个变量m与n

    如果我们想将m与n的值交换过来,可以这么做

    >>> temp=m
    >>> m=n
    >>> n=temp
    >>> m,n
    (20, 10)
    

    交叉赋值指的是一行代码可以搞定这件事

    >>> m=10
    >>> n=20
    >>> m,n=n,m # 交叉赋值
    >>> m,n
    (20, 10)
    

    2.3.4 解压赋值

    如果我们想把列表中的多个值取出来依次赋值给多个变量名,可以这么做

    >>> nums=[11,22,33,44,55]
    >>> 
    >>> a=nums[0]
    >>> b=nums[1]
    >>> c=nums[2]
    >>> d=nums[3]
    >>> e=nums[4]
    >>> a,b,c,d,e
    (11, 22, 33, 44, 55)
    

    解压赋值指的是一行代码可以搞定这件事

    >>> a,b,c,d,e=nums # nums包含多个值,就好比一个压缩包,解压赋值因此得名
    >>> a,b,c,d,e
    (11, 22, 33, 44, 55)
    

    注意,上述解压赋值,等号左边的变量名个数必须与右面包含值的个数相同,否则会报错

    #1、变量名少了
    >>> a,b=nums
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    ValueError: too many values to unpack (expected 2)
    
    #2、变量名多了
    >>> a,b,c,d,e,f=nums
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    ValueError: not enough values to unpack (expected 6, got 5)
    

    但如果我们只想取头尾的几个值,可以用*_匹配

    >>> a,b,*_=nums
    >>> a,b
    (11, 22)
    

    ps:字符串、字典、元组、集合类型都支持解压赋值

    2.4 逻辑运算符

    逻辑运算符用于连接多个条件,进行关联判断,会返回布尔值True或False

    img

    2.4.1 连续多个and

    可以用and连接多个条件,会按照从左到右的顺序依次判断,一旦某一个条件为False,则无需再往右判断,可以立即判定最终结果就为False,只有在所有条件的结果都为True的情况下,最终结果才为True。

    >>> 2 > 1 and 1 != 1 and True and 3 > 2 # 判断完第二个条件,就立即结束,得的最终结果为False
    False
    

    2.4.2 连续多个or

    可以用or连接多个条件,会按照从左到右的顺序依次判断,一旦某一个条件为True,则无需再往右判断,可以立即判定最终结果就为True,只有在所有条件的结果都为False的情况下,最终结果才为False

    >>> 2 > 1 or 1 != 1 or True or 3 > 2 # 判断完第一个条件,就立即结束,得的最终结果为True
    True
    

    2.4.3 优先级not>and>or

    #1、三者的优先级关系:not>and>or,同一优先级默认从左往右计算。
    >>> 3>4 and 4>3 or 1==3 and 'x' == 'x' or 3 >3
    False
    
    #2、最好使用括号来区别优先级,其实意义与上面的一样
    '''
    原理为:
    (1) not的优先级最高,就是把紧跟其后的那个条件结果取反,所以not与紧跟其后的条件不可分割
    
    (2) 如果语句中全部是用and连接,或者全部用or连接,那么按照从左到右的顺序依次计算即可
    
    (3) 如果语句中既有and也有or,那么先用括号把and的左右两个条件给括起来,然后再进行运算
    '''
    >>> (3>4 and 4>3) or (1==3 and 'x' == 'x') or 3 >3
    False 
    
    #3、短路运算:逻辑运算的结果一旦可以确定,那么就以当前处计算到的值作为最终结果返回
    >>> 10 and 0 or '' and 0 or 'abc' or 'egon' == 'dsb' and 333 or 10 > 4
    我们用括号来明确一下优先级
    >>> (10 and 0) or ('' and 0) or 'abc' or ('egon' == 'dsb' and 333) or 10 > 4
    短路:       0      ''            'abc'                    
                假     假              真
    
    返回:                            'abc'
    
    #4、短路运算面试题:
    >>> 1 or 3
    1
    >>> 1 and 3
    3
    >>> 0 and 2 and 1
    0
    >>> 0 and 2 or 1
    1
    >>> 0 and 2 or 1 or 4
    1
    >>> 0 or False and 1
    False 
    

    2.5 成员运算符

    img

    注意:虽然下述两种判断可以达到相同的效果,但我们推荐使用第二种格式,因为not in语义更加明确

    >>> not 'lili' in ['jack','tom','robin']
    True
    >>> 'lili' not in ['jack','tom','robin']
    True
    

    2.6 身份运算符

    img

    需要强调的是:==双等号比较的是value是否相等,而is比较的是id是否相等

    #1. id相同,内存地址必定相同,意味着type和value必定相同
    #2. value相同type肯定相同,但id可能不同,如下
    >>> x='Info Tony:18'
    >>> y='Info Tony:18'
    >>> id(x),id(y) # x与y的id不同,但是二者的值相同
    (4327422640, 4327422256)
    
    
    >>> x == y # 等号比较的是value
    True
    >>> type(x),type(y) # 值相同type肯定相同
    (<class 'str'>, <class 'str'>)
    >>> x is y # is比较的是id,x与y的值相等但id可以不同
    False
    
    

    7、Python语法入门之流程控制

    一 引子:

    流程控制即控制流程,具体指控制程序的执行流程,而程序的执行流程分为三种结构:顺序结构(之前我们写的代码都是顺序结构)、分支结构(用到if判断)、循环结构(用到while与for)

    二 分支结构

    2.1 什么是分支结构

    分支结构就是根据条件判断的真假去执行不同分支对应的子代码

    2.2 为什么要用分支结构

    人类某些时候需要根据条件来决定做什么事情,比如:如果今天下雨,就带伞

    所以程序中必须有相应的机制来控制计算机具备人的这种判断能力

    2.3 如何使用分支结构

    2.3.1 if语法

    用if关键字来实现分支结构,完整语法如下

    if 条件1:   # 如果条件1的结果为True,就依次执行:代码1、代码2,......
       代码1
        代码2
        ......
    elif 条件2: # 如果条件2的结果为True,就依次执行:代码3、代码4,......
       代码3
        代码4
        ......
    elif 条件3: # 如果条件3的结果为True,就依次执行:代码5、代码6,......
       代码5
        代码6
        ......
    else:     # 其它情况,就依次执行:代码7、代码8,......
        代码7
        代码8
        ......
    # 注意:
    # 1、python用相同缩进(4个空格表示一个缩进)来标识一组代码块,同一组代码会自上而下依次运行
    # 2、条件可以是任意表达式,但执行结果必须为布尔类型
         # 在if判断中所有的数据类型也都会自动转换成布尔类型
           # 2.1、None,0,空(空字符串,空列表,空字典等)三种情况下转换成的布尔值为False
           # 2.2、其余均为True
    

    2.3.2 if应用案例

    案例1:

    如果:女人的年龄>30岁,那么:叫阿姨

    age_of_girl=31
    if age_of_girl > 30:
        print('阿姨好')
    

    案例2:

    如果:女人的年龄>30岁,那么:叫阿姨,否则:叫小姐

    age_of_girl=18
    if age_of_girl > 30:
        print('阿姨好')
    else:
        print('小姐好')
    

    案例3:

    如果:女人的年龄>=18并且<22岁并且身高>170并且体重<100并且是漂亮的,那么:表白,否则:叫阿姨**

    age_of_girl=18
    height=171
    weight=99
    is_pretty=True
    if age_of_girl >= 18 and age_of_girl < 22 and height > 170 and weight < 100 and is_pretty == True:
        print('表白...')
    else:
        print('阿姨好')
    

    案例4:

    如果:成绩>=90,那么:优秀

    如果成绩>=80且<90,那么:良好

    如果成绩>=70且<80,那么:普通

    其他情况:很差

    score=input('>>: ')
    score=int(score)
    
    if score >= 90:
        print('优秀')
    elif score >= 80:
        print('良好')
    elif score >= 70:
        print('普通')
    else:
        print('很差')
    

    案例 5:if 嵌套

    #在表白的基础上继续:
    #如果表白成功,那么:在一起
    #否则:打印。。。
    
    age_of_girl=18
    height=171
    weight=99
    is_pretty=True
    success=False
    
    if age_of_girl >= 18 and age_of_girl < 22 and height > 170 and weight < 100 and is_pretty == True:
        if success:
            print('表白成功,在一起')
        else:
            print('什么爱情不爱情的,爱nmlgb的爱情,爱nmlg啊...')
    else:
        print('阿姨好')
    

    练习1: 登陆功能

    name=input('请输入用户名字:').strip()
    password=input('请输入密码:').strip()
    if name == 'tony' and password == '123':
        print('tony login success')
    else:
        print('用户名或密码错误')
    

    练习2:

    #!/usr/bin/env python
    #根据用户输入内容打印其权限
    
    '''
    egon --> 超级管理员
    tom  --> 普通管理员
    jack,rain --> 业务主管
    其他 --> 普通用户
    '''
    name=input('请输入用户名字:')
    
    if name == 'egon':
        print('超级管理员')
    elif name == 'tom':
        print('普通管理员')
    elif name == 'jack' or name == 'rain':
        print('业务主管')
    else:
        print('普通用户')
    

    三 循环结构

    3.1 什么是循环结构

    循环结构就是重复执行某段代码块

    3.2 为什么要用循环结构

    人类某些时候需要重复做某件事情

    所以程序中必须有相应的机制来控制计算机具备人的这种循环做事的能力

    3.3 如何使用循环结构

    3.3.1 while循环语法

    python中有while与for两种循环机制,其中while循环称之为条件循环,语法如下

    while 条件:
         代码1     
         代码2     
         代码3
    while的运行步骤:
    步骤1:如果条件为真,那么依次执行:代码1、代码2、代码3、......
    步骤2:执行完毕后再次判断条件,如果条件为True则再次执行:代码1、代码2、代码3、......,如果条件为False,则循环终止
    

    img

    3.3.2 while循环应用案例

    案例一:while循环的基本使用

    用户认证程序

    #用户认证程序的基本逻辑就是接收用户输入的用户名密码然后与程序中存放的用户名密码进行判断,判断成功则登陆成功,判断失败则输出账号或密码错误
    username = "jason"
    password = "123"
    
    inp_name =  input("请输入用户名:")
    inp_pwd =  input("请输入密码:")
    if inp_name == username and inp_pwd == password:
        print("登陆成功")
    else:
        print("输入的用户名或密码错误!")
    #通常认证失败的情况下,会要求用户重新输入用户名和密码进行验证,如果我们想给用户三次试错机会,本质就是将上述代码重复运行三遍,你总不会想着把代码复制3次吧。。。。
    username = "jason"
    password = "123"
    
    # 第一次验证
    inp_name =  input("请输入用户名:")
    inp_pwd =  input("请输入密码:")
    if inp_name == username and inp_pwd == password:
        print("登陆成功")
    else:
        print("输入的用户名或密码错误!")
    
    # 第二次验证
    inp_name =  input("请输入用户名:")
    inp_pwd =  input("请输入密码:")
    if inp_name == username and inp_pwd == password:
        print("登陆成功")
    else:
        print("输入的用户名或密码错误!")
    
    # 第三次验证
    inp_name =  input("请输入用户名:")
    inp_pwd =  input("请输入密码:")
    if inp_name == username and inp_pwd == password:
        print("登陆成功")
    else:
        print("输入的用户名或密码错误!")
    
    #即使是小白的你,也觉得的太low了是不是,以后要修改功能还得修改3次,因此记住,写重复的代码是程序员最不耻的行为。
    #那么如何做到不用写重复代码又能让程序重复一段代码多次呢? 循环语句就派上用场啦(使用while循环实现)
    
    username = "jason"
    password = "123"
    # 记录错误验证的次数
    count = 0
    while count < 3:
        inp_name = input("请输入用户名:")
        inp_pwd = input("请输入密码:")
        if inp_name == username and inp_pwd == password:
            print("登陆成功")
        else:
            print("输入的用户名或密码错误!")
            count += 1
    

    案例二:while+break的使用

    使用了while循环后,代码确实精简多了,但问题是用户输入正确的用户名密码以后无法结束循环,那如何结束掉一个循环呢?这就需要用到break了!

    username = "jason"
    password = "123"
    # 记录错误验证的次数
    count = 0
    while count < 3:
        inp_name = input("请输入用户名:")
        inp_pwd = input("请输入密码:")
        if inp_name == username and inp_pwd == password:
            print("登陆成功")
            break # 用于结束本层循环
        else:
            print("输入的用户名或密码错误!")
            count += 1
    

    案例三:while循环嵌套+break

    如果while循环嵌套了很多层,要想退出每一层循环则需要在每一层循环都有一个break

    username = "jason"
    password = "123"
    count = 0
    while count < 3:  # 第一层循环
        inp_name = input("请输入用户名:")
        inp_pwd = input("请输入密码:")
        if inp_name == username and inp_pwd == password:
            print("登陆成功")
            while True:  # 第二层循环
                cmd = input('>>: ')
                if cmd == 'quit':
                    break  # 用于结束本层循环,即第二层循环
                print('run <%s>' % cmd)
            break  # 用于结束本层循环,即第一层循环
        else:
            print("输入的用户名或密码错误!")
            count += 1
    

    案例四:while循环嵌套+tag的使用

    针对嵌套多层的while循环,如果我们的目的很明确就是要在某一层直接退出所有层的循环,其实有一个窍门,就让所有while循环的条件都用同一个变量,该变量的初始值为True,一旦在某一层将该变量的值改成False,则所有层的循环都结束

    username = "jason"
    password = "123"
    count = 0
    
    tag = True
    while tag: 
        inp_name = input("请输入用户名:")
        inp_pwd = input("请输入密码:")
        if inp_name == username and inp_pwd == password:
            print("登陆成功")
            while tag:  
                cmd = input('>>: ')
                if cmd == 'quit':
                    tag = False  # tag变为False, 所有while循环的条件都变为False 
                    break
                print('run <%s>' % cmd)
            break  # 用于结束本层循环,即第一层循环
        else:
            print("输入的用户名或密码错误!")
            count += 1
    

    案例五:while+continue的使用

    break代表结束本层循环,而continue则用于结束本次循环,直接进入下一次循环

    # 打印1到10之间,除7以外的所有数字
    number=11
    while number>1:
        number -= 1
        if number==7:
            continue # 结束掉本次循环,即本次循环continue之后的代码都不会运行了,而是直接进入下一次循环
        print(number)
    

    案例五:while+else的使用

    在while循环的后面,我们可以跟else语句,当while 循环正常执行完并且中间没有被break 中止的话,就会执行else后面的语句,所以我们可以用else来验证,循环是否正常结束

    count = 0
    while count <= 5 :
        count += 1
        print("Loop",count)
    else:
        print("循环正常执行完啦")
    print("-----out of while loop ------")
    输出
    Loop 1
    Loop 2
    Loop 3
    Loop 4
    Loop 5
    Loop 6
    循环正常执行完啦   #没有被break打断,所以执行了该行代码
    -----out of while loop ------
    

    如果执行过程中被break,就不会执行else的语句

    count = 0
    while count <= 5 :
        count += 1
        if count == 3:
            break
        print("Loop",count)
    else:
        print("循环正常执行完啦")
    print("-----out of while loop ------")
    输出
    Loop 1
    Loop 2
    -----out of while loop ------ #由于循环被break打断了,所以不执行else后的输出语句
    

    练习1:

    寻找1到100之间数字7最大的倍数(结果是98)

    number = 100
    while number > 0:
    	if number %7 == 0:
    		print(number)
    		break
    	number -= 1
    

    练习2:

    age=18
    count=0
    while count<3:
        count+=1
        guess = int(input(">>:"))
        if guess > age :
            print("猜的太大了,往小里试试...")
        elif guess < age :
            print("猜的太小了,往大里试试...")
        else:
            print("恭喜你,猜对了...")
    

    3.3.3 for循环语法

    循环结构的第二种实现方式是for循环,for循环可以做的事情while循环都可以实现,之所以用for循环是因为在循环取值(即遍历值)时for循环比while循环的使用更为简洁,

    for循环语法如下

    for 变量名 in 可迭代对象: # 此时只需知道可迭代对象可以是字符串\列表\字典,我们之后会专门讲解可迭代对象
        代码一
        代码二
        ...
    
    #例1
    for item in ['a','b','c']:
        print(item)
    # 运行结果
    a
    b
    c
    
    # 参照例1来介绍for循环的运行步骤
    # 步骤1:从列表['a','b','c']中读出第一个值赋值给item(item=‘a’),然后执行循环体代码
    # 步骤2:从列表['a','b','c']中读出第二个值赋值给item(item=‘b’),然后执行循环体代码
    # 步骤3: 重复以上过程直到列表中的值读尽
    

    img

    3.3.4 for循环应用案例

    # 简单版:for循环的实现方式
    for count in range(6):  # range(6)会产生从0-5这6个数
        print(count)
    
    # 复杂版:while循环的实现方式
    count = 0
    while count < 6:
        print(count)
        count += 1
    

    案例二:遍历字典

    # 简单版:for循环的实现方式
    for k in {'name':'jason','age':18,'gender':'male'}:  # for 循环默认取的是字典的key赋值给变量名k
        print(k)
    
    # 复杂版:while循环确实可以遍历字典,后续将会迭代器部分详细介绍
    

    案例三:for循环嵌套

    #请用for循环嵌套的方式打印如下图形:
    *****
    *****
    *****
    
    for i in range(3):
        for j in range(5):
            print("*",end='')
        print()  # print()表示换行
    

    注意:break 与 continue也可以用于for循环,使用语法同while循环

    练习一:

    打印九九乘法表

    for i in range(1,10):
        for j in range(1,i+1):
            print('%s*%s=%s' %(i,j,i*j),end=' ')
        print()
    

    练习二:

    打印金字塔

    # 分析
    '''
    #max_level=5
         *        # current_level=1,空格数=4,*号数=1
        ***       # current_level=2,空格数=3,*号数=3
       *****      # current_level=3,空格数=2,*号数=5
      *******     # current_level=4,空格数=1,*号数=7
     *********    # current_level=5,空格数=0,*号数=9
    
    # 数学表达式
    空格数=max_level-current_level
    *号数=2*current_level-1
    '''
    # 实现:
    max_level=5
    for current_level in range(1,max_level+1):
        for i in range(max_level-current_level):
            print(' ',end='') #在一行中连续打印多个空格
        for j in range(2*current_level-1):
            print('*',end='') #在一行中连续打印多个空格
        print()
    

    8、基本数据类型及内置方法

    一 引子

    数据类型是用来记录事物状态的,而事物的状态是不断变化的(如:一个人年龄的增长(操作int类型) ,单个人名的修改(操作str类型),学生列表中增加学生(操作list类型)等),这意味着我们在开发程序时需要频繁对数据进行操作,为了提升我们的开发效率, python针对这些常用的操作,为每一种数据类型内置了一系列方法。本章的主题就是带大家详细了解下它们,以及每种数据类型的详细定义、类型转换。

    二 数字类型int与float

    2.1 定义

    # 1、定义:
    # 1.1 整型int的定义
    age=10  # 本质age = int(10)
    
    # 1.2 浮点型float的定义
    salary=3000.3  # 本质salary=float(3000.3)
    
    # 注意:名字+括号的意思就是调用某个功能,比如
    # print(...)调用打印功能
    # int(...)调用创建整型数据的功能
    # float(...)调用创建浮点型数据的功能
    

    2.2 类型转换

    # 1、数据类型转换
    # 1.1 int可以将由纯整数构成的字符串直接转换成整型,若包含其他任意非整数符号,则会报错
    >>> s = '123'
    >>> res = int(s)
    >>> res,type(res)
    (123, <class 'int'>)
    
    >>> int('12.3') # 错误演示:字符串内包含了非整数符号.
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    ValueError: invalid literal for int() with base 10: '12.3'
    
    # 1.2 进制转换
    # 十进制转其他进制
    >>> bin(3)
    '0b11'
    >>> oct(9)
    '0o11'
    >>> hex(17)
    '0x11'
    # 其他进制转十进制
    >>> int('0b11',2)
    3
    >>> int('0o11',8)
    9
    >>> int('0x11',16)
    17
    
    # 1.3 float同样可以用来做数据类型的转换
    >>> s = '12.3'
    >>> res=float(s)
    >>> res,type(res)
    (12.3, <class 'float'>)
    

    2.3 使用

    数字类型主要就是用来做数学运算与比较运算,因此数字类型除了与运算符结合使用之外,并无需要掌握的内置方法

    三 字符串

    3.1 定义:

    # 定义:在单引号\双引号\三引号内包含一串字符
    name1 = 'jason'  # 本质:name = str('任意形式内容')
    name2 = "lili"  # 本质:name = str("任意形式内容")
    name3 = """ricky"""  # 本质:name = str("""任意形式内容""")
    

    3.2 类型转换

    # 数据类型转换:str()可以将任意数据类型转换成字符串类型,例如 
    >>> type(str([1,2,3])) # list->str
    <class 'str'>
    >>> type(str({"name":"jason","age":18})) # dict->str
    <class 'str'>
    >>> type(str((1,2,3)))  # tuple->str
    <class 'str'>
    >>> type(str({1,2,3,4})) # set->str
    <class 'str'>
    

    3.3 使用

    3.3.1 优先掌握的操作

    >>> str1 = 'hello python!'
    
    
    # 1.按索引取值(正向取,反向取):
    # 1.1 正向取(从左往右)
    >>> str1[6]
    p
    # 1.2 反向取(负号表示从右往左)
    >>> str1[-4]
    h
    # 1.3 对于str来说,只能按照索引取值,不能改
    >>> str1[0]='H' # 报错TypeError
    
    
    # 2.切片(顾头不顾尾,步长)
    # 2.1 顾头不顾尾:取出索引为0到8的所有字符
    >>> str1[0:9]  
    hello pyt
    # 2.2 步长:0:9:2,第三个参数2代表步长,会从0开始,每次累加一个2即可,所以会取出索引0、2、4、6、8的字符
    >>> str1[0:9:2]  
    hlopt 
    # 2.3 反向切片
    >>> str1[::-1]  # -1表示从右往左依次取值
    !nohtyp olleh
    
    # 3.长度len
    # 3.1 获取字符串的长度,即字符的个数,但凡存在于引号内的都算作字符)
    >>> len(str1) # 空格也算字符
    13
    
    # 4.成员运算 in 和 not in    
    # 4.1 int:判断hello 是否在 str1里面
    >>> 'hello' in str1  
    True
    # 4.2 not in:判断tony 是否不在 str1里面
    >>> 'tony' not in str1 
    True
    
    # 5.strip移除字符串首尾指定的字符(默认移除空格)
    # 5.1 括号内不指定字符,默认移除首尾空白字符(空格、\n、\t)
    >>> str1 = '  life is short!  '
    >>> str1.strip()  
    life is short!
    
    # 5.2 括号内指定字符,移除首尾指定的字符
    >>> str2 = '**tony**'  
    >>> str2.strip('*')  
    tony
    
    # 6.切分split
    # 6.1 括号内不指定字符,默认以空格作为切分符号
    >>> str3='hello world'
    >>> str3.split()
    ['hello', 'world']
    # 6.2 括号内指定分隔字符,则按照括号内指定的字符切割字符串
    >>> str4 = '127.0.0.1'
    >>> str4.split('.')  
    ['127', '0', '0', '1']  # 注意:split切割得到的结果是列表数据类型
    
    
    # 7.循环
    >>> str5 = '今天你好吗?'
    >>> for line in str5:  # 依次取出字符串中每一个字符
    ...     print(line)
    ...
    今
    天
    你
    好
    吗
    ?
    

    3.3.2 需要掌握的操作

    1. strip, lstrip, rstrip

    >>> str1 = '**tony***'
    
    >>> str1.strip('*')  # 移除左右两边的指定字符
    'tony'
    >>> str1.lstrip('*')  # 只移除左边的指定字符
    tony***
    >>> str1.rstrip('*')  # 只移除右边的指定字符
    **tony
    

    2. lower(),upper()

    >>> str2 = 'My nAme is tonY!'
    
    >>> str2.lower()  # 将英文字符串全部变小写
    my name is tony!
    >>> str2.upper()  # 将英文字符串全部变大写
    MY NAME IS TONY!
    

    3. startswith,endswith

    >>> str3 = 'tony jam'
    
    # startswith()判断字符串是否以括号内指定的字符开头,结果为布尔值True或False
    >>> str3.startswith('t') 
    True
    >>> str3.startswith('j')
    False
    # endswith()判断字符串是否以括号内指定的字符结尾,结果为布尔值True或False
    >>> str3.endswith('jam')
    True
    >>> str3.endswith('tony')  
    False
    

    4.格式化输出之format

    之前我们使用%s来做字符串的格式化输出操作,在传值时,必须严格按照位置与%s一一对应,而字符串的内置方法format则提供了一种不依赖位置的传值方式

    案例:

    # format括号内在传参数时完全可以打乱顺序,但仍然能指名道姓地为指定的参数传值,name=‘tony’就是传给{name}
    >>> str4 = 'my name is {name}, my age is {age}!'.format(age=18,name='tony')
    >>> str4  
    'my name is tony, my age is 18!'
    
    >>> str4 = 'my name is {name}{name}{name}, my age is {name}!'.format(name='tony', age=18)
    >>> str4  
    'my name is tonytonytony, my age is tony!'
    

    format的其他使用方式(了解)

    # 类似于%s的用法,传入的值会按照位置与{}一一对应
    >>> str4 = 'my name is {}, my age is {}!'.format('tony', 18)
    >>> str4 
    my name is tony, my age is 18!
    # 把format传入的多个值当作一个列表,然后用{索引}取值
    >>> str4 = 'my name is {0}, my age is {1}!'.format('tony', 18)
    >>> str4
    my name is tony, my age is 18!
    
    >>> str4 = 'my name is {1}, my age is {0}!'.format('tony', 18)
    >>> str4  
    my name is 18, my age is tony!
    
    >>> str4 = 'my name is {1}, my age is {1}!'.format('tony', 18)
    >>> str4  
    my name is 18, my age is 18!
    

    5.split,rsplit

    # split会按照从左到右的顺序对字符串进行切分,可以指定切割次数
    >>> str5='C:/a/b/c/d.txt'
    >>> str5.split('/',1)
    ['C:', 'a/b/c/d.txt']  
    
    # rsplit刚好与split相反,从右往左切割,可以指定切割次数
    >>> str5='a|b|c'
    >>> str5.rsplit('|',1)
    ['a|b', 'c']
    

    6. join

    # 从可迭代对象中取出多个字符串,然后按照指定的分隔符进行拼接,拼接的结果为字符串
    >>> '%'.join('hello') # 从字符串'hello'中取出多个字符串,然后按照%作为分隔符号进行拼接
    'h%e%l%l%o'
    >>> '|'.join(['tony','18','read'])  # 从列表中取出多个字符串,然后按照*作为分隔符号进行拼接
    'tony|18|read'
    

    7. replace

    # 用新的字符替换字符串中旧的字符
    >>> str7 = 'my name is tony, my age is 18!'  # 将tony的年龄由18岁改成73岁
    >>> str7 = str7.replace('18', '73')  # 语法:replace('旧内容', '新内容')
    >>> str7
    my name is tony, my age is 73!
    
    # 可以指定修改的个数
    >>> str7 = 'my name is tony, my age is 18!'
    >>> str7 = str7.replace('my', 'MY',1) # 只把一个my改为MY
    >>> str7
    'MY name is tony, my age is 18!'
    

    8.isdigit

    # 判断字符串是否是纯数字组成,返回结果为True或False
    >>> str8 = '5201314'
    >>> str8.isdigit()
    True
    
    >>> str8 = '123g123'
    >>> str8.isdigit()
    False
    

    3.3.3 了解操作

    # 1.find,rfind,index,rindex,count
    # 1.1 find:从指定范围内查找子字符串的起始索引,找得到则返回数字1,找不到则返回-1
    >>> msg='tony say hello'
    >>> msg.find('o',1,3)  # 在索引为1和2(顾头不顾尾)的字符中查找字符o的索引
    1  
    # 1.2 index:同find,但在找不到时会报错
    >>> msg.index('e',2,4) # 报错ValueError
    # 1.3 rfind与rindex:略
    # 1.4 count:统计字符串在大字符串中出现的次数
    >>> msg = "hello everyone"
    >>> msg.count('e')  # 统计字符串e出现的次数
    4
    >>> msg.count('e',1,6)  # 字符串e在索引1~5范围内出现的次数
    1
    
    # 2.center,ljust,rjust,zfill
    >>> name='tony'
    >>> name.center(30,'-')  # 总宽度为30,字符串居中显示,不够用-填充
    -------------tony-------------
    >>> name.ljust(30,'*')  # 总宽度为30,字符串左对齐显示,不够用*填充
    tony**************************
    >>> name.rjust(30,'*')  # 总宽度为30,字符串右对齐显示,不够用*填充
    **************************tony
    >>> name.zfill(50)  # 总宽度为50,字符串右对齐显示,不够用0填充
    0000000000000000000000000000000000000000000000tony
    
    # 3.expandtabs
    >>> name = 'tony\thello'  # \t表示制表符(tab键)
    >>> name
    tony    hello
    >>> name.expandtabs(1)  # 修改\t制表符代表的空格数
    tony hello
    
    # 4.captalize,swapcase,title
    # 4.1 captalize:首字母大写
    >>> message = 'hello everyone nice to meet you!'
    >>> message.capitalize()
    Hello everyone nice to meet you!  
    # 4.2 swapcase:大小写翻转
    >>> message1 = 'Hi girl, I want make friends with you!'
    >>> message1.swapcase()  
    hI GIRL, i WANT MAKE FRIENDS WITH YOU!  
    #4.3 title:每个单词的首字母大写
    >>> msg = 'dear my friend i miss you very much'
    >>> msg.title()
    Dear My Friend I Miss You Very Much 
    
    # 5.is数字系列
    #在python3中
    num1 = b'4' #bytes
    num2 = u'4' #unicode,python3中无需加u就是unicode
    num3 = '四' #中文数字
    num4 = 'Ⅳ' #罗马数字
    
    #isdigt:bytes,unicode
    >>> num1.isdigit()
    True
    >>> num2.isdigit()
    True
    >>> num3.isdigit()
    False
    >>> num4.isdigit() 
    False
    
    #isdecimal:uncicode(bytes类型无isdecimal方法)
    >>> num2.isdecimal() 
    True
    >>> num3.isdecimal() 
    False
    >>> num4.isdecimal() 
    False
    
    #isnumberic:unicode,中文数字,罗马数字(bytes类型无isnumberic方法)
    >>> num2.isnumeric() 
    True
    >>> num3.isnumeric() 
    True
    >>> num4.isnumeric() 
    True
    
    # 三者不能判断浮点数
    >>> num5 = '4.3'
    >>> num5.isdigit()
    False
    >>> num5.isdecimal()
    False
    >>> num5.isnumeric()
    False
    
    '''
    总结:
        最常用的是isdigit,可以判断bytes和unicode类型,这也是最常见的数字应用场景
        如果要判断中文数字或罗马数字,则需要用到isnumeric。
    '''
    
    # 6.is其他
    >>> name = 'tony123'
    >>> name.isalnum() #字符串中既可以包含数字也可以包含字母
    True
    >>> name.isalpha() #字符串中只包含字母
    False
    >>> name.isidentifier()
    True
    >>> name.islower()  # 字符串是否是纯小写
    True
    >>> name.isupper()  # 字符串是否是纯大写
    False
    >>> name.isspace()  # 字符串是否全是空格
    False
    >>> name.istitle()  # 字符串中的单词首字母是否都是大写
    False
    

    四 列表

    4.1 定义

    # 定义:在[]内,用逗号分隔开多个任意数据类型的值
    l1 = [1,'a',[1,2]]  # 本质:l1 = list([1,'a',[1,2]])
    

    4.2 类型转换

    # 但凡能被for循环遍历的数据类型都可以传给list()转换成列表类型,list()会跟for循环一样遍历出数据类型中包含的每一个元素然后放到列表中
    >>> list('wdad') # 结果:['w', 'd', 'a', 'd'] 
    >>> list([1,2,3]) # 结果:[1, 2, 3]
    >>> list({"name":"jason","age":18}) #结果:['name', 'age']
    >>> list((1,2,3)) # 结果:[1, 2, 3] 
    >>> list({1,2,3,4}) # 结果:[1, 2, 3, 4]
    

    4.3 使用

    4.3.1 优先掌握的操作

    # 1.按索引存取值(正向存取+反向存取):即可存也可以取  
    # 1.1 正向取(从左往右)
    >>> my_friends=['tony','jason','tom',4,5]
    >>> my_friends[0]  
    tony
    # 1.2 反向取(负号表示从右往左)
    >>> my_friends[-1]  
    5
    # 1.3 对于list来说,既可以按照索引取值,又可以按照索引修改指定位置的值,但如果索引不存在则报错
    >>> my_friends = ['tony','jack','jason',4,5]
    >>> my_friends[1] = 'martthow'
    >>> my_friends
    ['tony', 'martthow', 'jason', 4, 5]
    
    
    # 2.切片(顾头不顾尾,步长)
    # 2.1 顾头不顾尾:取出索引为0到3的元素
    >>> my_friends[0:4] 
    ['tony', 'jason', 'tom', 4]
    # 2.2 步长:0:4:2,第三个参数2代表步长,会从0开始,每次累加一个2即可,所以会取出索引0、2的元素
    >>> my_friends[0:4:2]  
    ['tony', 'tom']
    
    # 3.长度
    >>> len(my_friends)
    5
    
    # 4.成员运算in和not in
    >>> 'tony' in my_friends
    True
    >>> 'xxx' not in my_friends
    True
    
    # 5.添加
    # 5.1 append()列表尾部追加元素
    >>> l1 = ['a','b','c']
    >>> l1.append('d')
    >>> l1
    ['a', 'b', 'c', 'd']
    
    # 5.2 extend()一次性在列表尾部添加多个元素
    >>> l1.extend(['a','b','c'])
    >>> l1
    ['a', 'b', 'c', 'd', 'a', 'b', 'c']
    
    # 5.3 insert()在指定位置插入元素
    >>> l1.insert(0,"first")  # 0表示按索引位置插值
    >>> l1
    ['first', 'a', 'b', 'c', 'alisa', 'a', 'b', 'c']
    
    # 6.删除
    # 6.1 del
    >>> l = [11,22,33,44]
    >>> del l[2]  # 删除索引为2的元素
    >>> l
    [11,22,44]
    
    # 6.2 pop()默认删除列表最后一个元素,并将删除的值返回,括号内可以通过加索引值来指定删除元素
    >>> l = [11,22,33,22,44]
    >>> res=l.pop()
    >>> res
    44
    >>> res=l.pop(1)
    >>> res
    22
    
    # 6.3 remove()括号内指名道姓表示要删除哪个元素,没有返回值
    >>> l = [11,22,33,22,44]
    >>> res=l.remove(22) # 从左往右查找第一个括号内需要删除的元素
    >>> print(res)
    None
    
    # 7.reverse()颠倒列表内元素顺序
    >>> l = [11,22,33,44]
    >>> l.reverse() 
    >>> l
    [44,33,22,11]
    
    # 8.sort()给列表内所有元素排序
    # 8.1 排序时列表元素之间必须是相同数据类型,不可混搭,否则报错
    >>> l = [11,22,3,42,7,55]
    >>> l.sort()
    >>> l 
    [3, 7, 11, 22, 42, 55]  # 默认从小到大排序
    >>> l = [11,22,3,42,7,55]
    >>> l.sort(reverse=True)  # reverse用来指定是否跌倒排序,默认为False
    >>> l 
    [55, 42, 22, 11, 7, 3]
    # 8.2 了解知识:
    # 我们常用的数字类型直接比较大小,但其实,字符串、列表等都可以比较大小,原理相同:都是依次比较对应位置的元素的大小,如果分出大小,则无需比较下一个元素,比如
    >>> l1=[1,2,3]
    >>> l2=[2,]
    >>> l2 > l1
    True
    # 字符之间的大小取决于它们在ASCII表中的先后顺序,越往后越大
    >>> s1='abc'
    >>> s2='az'
    >>> s2 > s1 # s1与s2的第一个字符没有分出胜负,但第二个字符'z'>'b',所以s2>s1成立
    True
    # 所以我们也可以对下面这个列表排序
    >>> l = ['A','z','adjk','hello','hea']
    >>> l.sort()
    >>> l
    ['A', 'adjk', 'hea', 'hello','z']
    
    # 9.循环
    # 循环遍历my_friends列表里面的值
    for line in my_friends:
        print(line) 
    'tony'
    'jack'
    'jason'
    4
    5
    

    4.3.2 了解操作

    >>> l=[1,2,3,4,5,6]
    >>> l[0:3:1] 
    [1, 2, 3]  # 正向步长
    >>> l[2::-1] 
    [3, 2, 1]  # 反向步长
    
    # 通过索引取值实现列表翻转
    >>> l[::-1]
    [6, 5, 4, 3, 2, 1]
    

    五 元组

    5.1 作用

    元组与列表类似,也是可以存多个任意类型的元素,不同之处在于元组的元素不能修改,即元组相当于不可变的列表,用于记录多个固定不允许修改的值,单纯用于取

    5.2 定义方式

    # 在()内用逗号分隔开多个任意类型的值
    >>> countries = ("中国","美国","英国")  # 本质:countries = tuple("中国","美国","英国")
    # 强调:如果元组内只有一个值,则必须加一个逗号,否则()就只是包含的意思而非定义元组
    >>> countries = ("中国",)  # 本质:countries = tuple("中国")
    

    5.3 类型转换

    # 但凡能被for循环的遍历的数据类型都可以传给tuple()转换成元组类型
    >>> tuple('wdad') # 结果:('w', 'd', 'a', 'd') 
    >>> tuple([1,2,3]) # 结果:(1, 2, 3)
    >>> tuple({"name":"jason","age":18}) # 结果:('name', 'age')
    >>> tuple((1,2,3)) # 结果:(1, 2, 3)
    >>> tuple({1,2,3,4}) # 结果:(1, 2, 3, 4)
    # tuple()会跟for循环一样遍历出数据类型中包含的每一个元素然后放到元组中
    

    5.4 使用

    >>> tuple1 = (1, 'hhaha', 15000.00, 11, 22, 33) 
    # 1、按索引取值(正向取+反向取):只能取,不能改否则报错!  
    >>> tuple1[0]
    1
    >>> tuple1[-2]
    22
    >>> tuple1[0] = 'hehe'  # 报错:TypeError:
    
    # 2、切片(顾头不顾尾,步长)
    >>> tuple1[0:6:2] 
    (1, 15000.0, 22)
    
    # 3、长度
    >>> len(tuple1)  
    6
    
    # 4、成员运算 in 和 not in
    >>> 'hhaha' in tuple1 
    True
    >>> 'hhaha' not in tuple1  
    False 
    
    # 5、循环
    >>> for line in tuple1:
    ...     print(line)
    1
    hhaha
    15000.0
    11
    22
    33
    

    六 字典

    6.1 定义方式

    # 定义:在{}内用逗号分隔开多元素,每一个元素都是key:value的形式,其中value可以是任意类型,而key则必须是不可变类型,详见第八小节,通常key应该是str类型,因为str类型会对value有描述性的功能
    info={'name':'tony','age':18,'sex':'male'} #本质info=dict({....})
    # 也可以这么定义字典
    info=dict(name='tony',age=18,sex='male') # info={'age': 18, 'sex': 'male', 'name': 'tony'}
    

    6.2 类型转换

    # 转换1: 
    >>> info=dict([['name','tony'],('age',18)])
    >>> info
    {'age': 18, 'name': 'tony'}
    
    # 转换2:fromkeys会从元组中取出每个值当做key,然后与None组成key:value放到字典中
    >>> {}.fromkeys(('name','age','sex'),None)  
    {'age': None, 'sex': None, 'name': None}
    

    6.3 使用

    6.3.1 优先掌握的操作

    # 1、按key存取值:可存可取
    # 1.1 取
    >>> dic = {
    ...     'name': 'xxx',
    ...     'age': 18,
    ...     'hobbies': ['play game', 'basketball']
    ... }
    >>> dic['name']
    'xxx'
    >>> dic['hobbies'][1]
    'basketball'
    # 1.2 对于赋值操作,如果key原先不存在于字典,则会新增key:value
    >>> dic['gender'] = 'male'  
    >>> dic
    {'name': 'tony', 'age': 18, 'hobbies': ['play game', 'basketball'],'gender':'male'}
    # 1.3 对于赋值操作,如果key原先存在于字典,则会修改对应value的值
    >>> dic['name'] = 'tony'
    >>> dic
    {'name': 'tony', 'age': 18, 'hobbies': ['play game', 'basketball']}
    
    
    # 2、长度len
    >>> len(dic) 
    3
    
    
    # 3、成员运算in和not in
    >>> 'name' in dic  # 判断某个值是否是字典的key
    True
    
    
    # 4、删除
    >>> dic.pop('name')  # 通过指定字典的key来删除字典的键值对
    >>> dic
    {'age': 18, 'hobbies': ['play game', 'basketball']}
    
    
    # 5、键keys(),值values(),键值对items()
    >>> dic = {'age': 18, 'hobbies': ['play game', 'basketball'], 'name': 'xxx'}
    # 获取字典所有的key
    >>> dic.keys()  
    dict_keys(['name', 'age', 'hobbies'])
    # 获取字典所有的value
    >>> dic.values()
    dict_values(['xxx', 18, ['play game', 'basketball']])
    # 获取字典所有的键值对
    >>> dic.items()
    dict_items([('name', 'xxx'), ('age', 18), ('hobbies', ['play game', 'basketball'])])
    
    
    # 6、循环
    # 6.1 默认遍历的是字典的key
    >>> for key in dic:
    ...     print(key)
    ... 
    age
    hobbies
    name
    # 6.2 只遍历key
    >>> for key in dic.keys():
    ...     print(key)
    ... 
    age
    hobbies
    name
    # 6.3 只遍历value
    >>> for key in dic.values():
    ...     print(key)
    ... 
    18
    ['play game', 'basketball']
    xxx
    # 6.4 遍历key与value
    >>> for key in dic.items():
    ...     print(key)
    ... 
    ('age', 18)
    ('hobbies', ['play game', 'basketball'])
    ('name', 'xxx')
    

    6.3.2 需要掌握的操作

    1. get()

    >>> dic= {'k1':'jason','k2':'Tony','k3':'JY'}
    >>> dic.get('k1')
    'jason'  # key存在,则获取key对应的value值
    >>> res=dic.get('xxx') # key不存在,不会报错而是默认返回None
    >>> print(res)
    None  
    >>> res=dic.get('xxx',666) # key不存在时,可以设置默认返回的值
    >>> print(res)
    666 
    # ps:字典取值建议使用get方法
    

    2. pop()

    >>> dic= {'k1':'jason','k2':'Tony','k3':'JY'}
    >>> v = dic.pop('k2')  # 删除指定的key对应的键值对,并返回值
    >>> dic
    {'k1': 'jason', 'kk2': 'JY'}
    >>> v
    'Tony'
    

    3. popitem()

    >>> dic= {'k1':'jason','k2':'Tony','k3':'JY'}
    >>> item = dic.popitem()  # 随机删除一组键值对,并将删除的键值放到元组内返回
    >>> dic
    {'k3': 'JY', 'k2': 'Tony'}
    >>> item
    ('k1', 'jason')
    

    4. update()

    # 用新字典更新旧字典,有则修改,无则添加
    >>> dic= {'k1':'jason','k2':'Tony','k3':'JY'}
    >>> dic.update({'k1':'JN','k4':'xxx'})
    >>> dic
    {'k1': 'JN', 'k3': 'JY', 'k2': 'Tony', 'k4': 'xxx'}
    

    5. fromkeys()

    >>> dic = dict.fromkeys(['k1','k2','k3'],[])
    >>> dic
    {'k1': [], 'k2': [], 'k3': []}
    

    6. setdefault()

    # key不存在则新增键值对,并将新增的value返回
    >>> dic={'k1':111,'k2':222}
    >>> res=dic.setdefault('k3',333)
    >>> res
    333
    >>> dic # 字典中新增了键值对
    {'k1': 111, 'k3': 333, 'k2': 222}
    
    # key存在则不做任何修改,并返回已存在key对应的value值
    >>> dic={'k1':111,'k2':222}
    >>> res=dic.setdefault('k1',666)
    >>> res
    111
    >>> dic # 字典不变
    {'k1': 111, 'k2': 222}
    

    七 集合

    7.1 作用

    集合、list、tuple、dict一样都可以存放多个值,但是集合主要用于:去重、关系运算

    7.2 定义

    """
    定义:在{}内用逗号分隔开多个元素,集合具备以下三个特点:
         1:每个元素必须是不可变类型
         2:集合内没有重复的元素
         3:集合内元素无序
    """
    s = {1,2,3,4}  # 本质 s = set({1,2,3,4})
    
    # 注意1:列表类型是索引对应值,字典是key对应值,均可以取得单个指定的值,而集合类型既没有索引也没有key与值对应,所以无法取得单个的值,而且对于集合来说,主要用于去重与关系元素,根本没有取出单个指定值这种需求。
    
    # 注意2:{}既可以用于定义dict,也可以用于定义集合,但是字典内的元素必须是key:value的格式,现在我们想定义一个空字典和空集合,该如何准确去定义两者?
    d = {} # 默认是空字典 
    s = set() # 这才是定义空集合
    

    7.3 类型转换

    # 但凡能被for循环的遍历的数据类型(强调:遍历出的每一个值都必须为不可变类型)都可以传给set()转换成集合类型
    >>> s = set([1,2,3,4])
    >>> s1 = set((1,2,3,4))
    >>> s2 = set({'name':'jason',})
    >>> s3 = set('egon')
    >>> s,s1,s2,s3
    {1, 2, 3, 4} {1, 2, 3, 4} {'name'} {'e', 'o', 'g', 'n'}
    

    7.4 使用

    7.4.1 关系运算

    我们定义两个集合friends与friends2来分别存放两个人的好友名字,然后以这两个集合为例讲解集合的关系运算

    >>> friends1 = {"zero","kevin","jason","egon"} # 用户1的好友们 
    >>> friends2 = {"Jy","ricky","jason","egon"}   # 用户2的好友们
    

    两个集合的关系如下图所示

    img

    # 1.合集/并集(|):求两个用户所有的好友(重复好友只留一个)
    >>> friends1 | friends2
    {'kevin', 'ricky', 'zero', 'jason', 'Jy', 'egon'}
    
    # 2.交集(&):求两个用户的共同好友
    >>> friends1 & friends2
    {'jason', 'egon'}
    
    # 3.差集(-):
    >>> friends1 - friends2 # 求用户1独有的好友
    {'kevin', 'zero'}
    >>> friends2 - friends1 # 求用户2独有的好友
    {'ricky', 'Jy'}
    
    # 4.对称差集(^) # 求两个用户独有的好友们(即去掉共有的好友)
    >>> friends1 ^ friends2
    {'kevin', 'zero', 'ricky', 'Jy'}
    
    # 5.值是否相等(==)
    >>> friends1 == friends2
    False
    
    # 6.父集:一个集合是否包含另外一个集合
    # 6.1 包含则返回True
    >>> {1,2,3} > {1,2}
    True
    >>> {1,2,3} >= {1,2}
    True
    # 6.2 不存在包含关系,则返回False
    >>> {1,2,3} > {1,3,4,5}
    False
    >>> {1,2,3} >= {1,3,4,5}
    False
    
    
    # 7.子集
    >>> {1,2} < {1,2,3}
    True
    >>> {1,2} <= {1,2,3}
    True
    

    7.4.2 去重

    集合去重复有局限性

    # 1. 只能针对不可变类型
    # 2. 集合本身是无序的,去重之后无法保留原来的顺序
    

    示例如下

    >>> l=['a','b',1,'a','a']
    >>> s=set(l)
    >>> s # 将列表转成了集合
    {'b', 'a', 1}
    >>> l_new=list(s) # 再将集合转回列表
    >>> l_new
    ['b', 'a', 1] # 去除了重复,但是打乱了顺序
    
    # 针对不可变类型,并且保证顺序则需要我们自己写代码实现,例如
    l=[
        {'name':'lili','age':18,'sex':'male'},
        {'name':'jack','age':73,'sex':'male'},
        {'name':'tom','age':20,'sex':'female'},
        {'name':'lili','age':18,'sex':'male'},
        {'name':'lili','age':18,'sex':'male'},
    ]
    
    new_l=[]
    
    for dic in l:
        if dic not in new_l:
            new_l.append(dic)
    
    print(new_l)
    # 结果:既去除了重复,又保证了顺序,而且是针对不可变类型的去重
    [
        {'age': 18, 'sex': 'male', 'name': 'lili'}, 
        {'age': 73, 'sex': 'male', 'name': 'jack'}, 
        {'age': 20, 'sex': 'female', 'name': 'tom'}
    ]
    

    7.4.3 其他操作

    # 1.长度
    >>> s={'a','b','c'}
    >>> len(s)
    3
    
    # 2.成员运算
    >>> 'c' in s
    True
    
    # 3.循环
    >>> for item in s:
    ...     print(item)
    ... 
    c
    a
    b
    

    7.5 练习

    """
    一.关系运算
      有如下两个集合,pythons是报名python课程的学员名字集合,linuxs是报名linux课程的学员名字集合
      pythons={'jason','egon','kevin','ricky','gangdan','biubiu'}
      linuxs={'kermit','tony','gangdan'}
      1. 求出即报名python又报名linux课程的学员名字集合
      2. 求出所有报名的学生名字集合
      3. 求出只报名python课程的学员名字
      4. 求出没有同时这两门课程的学员名字集合
    """
    # 求出即报名python又报名linux课程的学员名字集合
    >>> pythons & linuxs
    # 求出所有报名的学生名字集合
    >>> pythons | linuxs
    # 求出只报名python课程的学员名字
    >>> pythons - linuxs
    # 求出没有同时这两门课程的学员名字集合
    >>> pythons ^ linuxs
    

    八 可变类型与不可变类型

    **可变数据类型:**值发生改变时,内存地址不变,即id不变,证明在改变原值

    **不可变类型:**值发生改变时,内存地址也发生改变,即id也变,证明是没有在改变原值,是产生了新的值

    数字类型:

    >>> x = 10
    >>> id(x)
    1830448896 
    >>> x = 20
    >>> id(x)
    1830448928
    # 内存地址改变了,说明整型是不可变数据类型,浮点型也一样
    

    img

    字符串

    >>> x = "Jy"
    >>> id(x)
    938809263920 
    >>> x = "Ricky"
    >>> id(x)
    938809264088
    # 内存地址改变了,说明字符串是不可变数据类型
    

    img

    列表

    >>> list1 = ['tom','jack','egon']
    >>> id(list1)
    486316639176
    >>> list1[2] = 'kevin'
    >>> id(list1)
    486316639176
    >>> list1.append('lili')
    >>> id(list1)
    486316639176
    # 对列表的值进行操作时,值改变但内存地址不变,所以列表是可变数据类型
    

    img

    元组

    >>> t1 = ("tom","jack",[1,2])
    >>> t1[0]='TOM' # 报错:TypeError
    >>> t1.append('lili') # 报错:TypeError
    # 元组内的元素无法修改,指的是元组内索引指向的内存地址不能被修改
    >>> t1 = ("tom","jack",[1,2])
    >>> id(t1[0]),id(t1[1]),id(t1[2])
    (4327403152, 4327403072, 4327422472)
    >>> t1[2][0]=111 # 如果元组中存在可变类型,是可以修改,但是修改后的内存地址不变
    >>> t1
    ('tom', 'jack', [111, 2])
    >>> id(t1[0]),id(t1[1]),id(t1[2]) # 查看id仍然不变
    (4327403152, 4327403072, 4327422472)
    

    img

    字典

    >>> dic = {'name':'egon','sex':'male','age':18}
    >>> 
    >>> id(dic)
    4327423112
    >>> dic['age']=19
    >>> dic
    {'age': 19, 'sex': 'male', 'name': 'egon'}
    >>> id(dic)
    4327423112
    # 对字典进行操作时,值改变的情况下,字典的id也是不变,即字典也是可变数据类型
    

    img

    九 数据类型总结

    img

    1、数字类型:
    2、字符串类型
    3、列表类型
    4、元组类型
    5、字典类型
    6、集合类型
    

    9、字符编码

    一 引入

    字符串类型、文本文件的内容都是由字符组成的,但凡涉及到字符的存取,都需要考虑字符编码的问题。

    二 知识储备

    2.1 三大核心硬件

    所有软件都是运行硬件之上的,与运行软件相关的三大核心硬件为cpu、内存、硬盘,我们需要明确三点

    #1、软件运行前,软件的代码及其相关数据都是存放于硬盘中的
    
    #2、任何软件的启动都是将数据从硬盘中读入内存,然后cpu从内存中取出指令并执行
    
    #3、软件运行过程中产生的数据最先都是存放于内存中的,若想永久保存软件产生的数据,则需要将数据由内存写入硬盘
    

    img

    2.2 文本编辑器读取文件内容的流程

    #阶段1、启动一个文件编辑器(文本编辑器如nodepad++,pycharm,word)
    
    #阶段2、文件编辑器会将文件内容从硬盘读入内存
    
    #阶段3、文本编辑器会将刚刚读入内存中的内容显示到屏幕上
    

    2.3 python解释器执行文件的流程

    以python test.py为例,执行流程如下

    #阶段1、启动python解释器,此时就相当于启动了一个文本编辑器
    
    #阶段2、python解释器相当于文本编辑器,从硬盘上将test.py的内容读入到内存中
    
    #阶段3、python解释器解释执行刚刚读入的内存的内容,开始识别python语法
    

    2.4 总结

    python解释器与文件本编辑的异同如下

    #1、相同点:前两个阶段二者完全一致,都是将硬盘中文件的内容读入内存,详解如下
    python解释器是解释执行文件内容的,因而python解释器具备读py文件的功能,这一点与文本编辑器一样
    
    #2、不同点:在阶段3时,针对内存中读入的内容处理方式不同,详解如下
    文本编辑器将文件内容读入内存后,是为了显示或者编辑,根本不去理会python的语法,而python解释器将文件内容读入内存后,可不是为了给你瞅一眼python代码写的啥,而是为了执行python代码、会识别python语法)
    

    三、字符编码介绍

    3.1 什么是字符编码?

    人类在与计算机交互时,用的都是人类能读懂的字符,如中文字符、英文字符、日文字符等

    而计算机只能识别二进制数,详解如下

    #二进制数即由0和1组成的数字,例如010010101010。计算机是基于电工作的,电的特性即高低电平,人类从逻辑层面将高电平对应为数字1,低电平对应为数字0,这直接决定了计算机可以识别的是由0和1组成的数字
    

    毫无疑问,由人类的字符到计算机中的数字,必须经历一个过程,如下

    img

    翻译的过程必须参照一个特定的标准,该标准称之为字符编码表,该表上存放的就是字符与数字一一对应的关系。

    字符编码中的编码指的是翻译或者转换的意思,即将人能理解的字符翻译成计算机能识别的数字

    3.2 字符编码表的发展史 (了解)

    字符编码的发展经历了三个重要的阶段,如下

    3.2.1 阶段一:一家独大

    现代计算机起源于美国,所以最先考虑仅仅是让计算机识别英文字符,于是诞生了ASCII表

    # ASCII表的特点:
        1、只有英文字符与数字的一一对应关系
        2、一个英文字符对应1Bytes,1Bytes=8bit,8bit最多包含256个数字,可以对应256个字符,足够表示所有英文字符
    

    3.2.2 阶段二:诸侯割据、天下大乱

    3.2.2 阶段二:诸侯割据、天下大乱

    为了让计算机能够识别中文和英文,中国人定制了GBK

    # GBK表的特点:
        1、只有中文字符、英文字符与数字的一一对应关系
        2、一个英文字符对应1Bytes
           一个中文字符对应2Bytes   
           补充说明:
           1Bytes=8bit,8bit最多包含256个数字,可以对应256个字符,足够表示所有英文字符
           2Bytes=16bit,16bit最多包含65536个数字,可以对应65536个字符,足够表示所有中文字符
    

    每个国家都各自的字符,为让计算机能够识别自己国家的字符外加英文字符,各个国家都制定了自己的字符编码表

    # Shift_JIS表的特点:
        1、只有日文字符、英文字符与数字的一一对应关系
    
    # Euc-kr表的特点:
        1、只有韩文字符、英文字符与数字的一一对应关系
    

    此时,美国人用的计算机里使用字符编码标准是ASCII、中国人用的计算机里使用字符编码标准是GBK、日本人用的计算机里使用字符编码标准是Shift_JIS,如下图所示,

    img

    字符编码发展到了这个阶段,可以用一句话概括:诸侯割据、天下大乱,详解如下

    图1中,文本编辑存取文件的原理如下

    文本文件内容全都为字符,无论存取都是涉及到字符编码问题
    #1、存文本文件
    人类通过文本编辑器输入的字符会被转化成ASCII格式的二进制存放于内存中,如果需要永久保存,则直接将内存中的ASCII格式的二进制写入硬盘
    
    #2、读文本文件
    直接将硬盘中的ASCII格式的二进制读入内存,然后通过ASCII表反解成英文字符
    

    图2图3都是相同的过程,此时无论是存还是取由于采用的字符编码表一样,所以肯定不会出现乱码问题,但问题是在美国人用的计算机里只能输入英文字符,而在中国人用的计算机里只能输入中文字符和英文字符…,毫无疑问我们希望计算机允许我们输入万国字符均可识别、不乱码,而现阶段计算机采用的字符编码ASCII、GBK、Shift_JIS都无法识别万国字符,所以我们必须定制一个兼容万国字符的编码表,请看阶段三

    3.2.3 阶段三:分久必合

    unicode于1990年开始研发,1994年正式公布,具备两大特点:

    1. 存在所有语言中的所有字符与数字的一一对应关系,即兼容万国字符
    
    #2. 与传统的字符编码的二进制数都有对应关系,详解如下
    

    很多地方或老的系统、应用软件仍会采用各种各样传统的编码,这是历史遗留问题。此处需要强调:软件是存放于硬盘的,而运行软件是要将软件加载到内存的,面对硬盘中存放的各种传统编码的软件,想让我们的计算机能够将它们全都正常运行而不出现乱码,内存中必须有一种兼容万国的编码,并且该编码需要与其他编码有相对应的映射/转换关系,这就是unicode的第二大特点产生的缘由

    img

    文本编辑器输入任何字符都是最新存在于内存中,是unicode编码的,存放于硬盘中,则可以转换成任意其他编码,只要该编码可以支持相应的字符

    # 英文字符可以被ASCII识别
    英文字符--->unciode格式的数字--->ASCII格式的数字
    
    # 中文字符、英文字符可以被GBK识别
    中文字符、英文字符--->unicode格式的数字--->gbk格式的数字
    
    # 日文字符、英文字符可以被shift-JIS识别
    日文字符、英文字符--->unicode格式的数字--->shift-JIS格式的数字
    

    3.3 编码与解码

    由字符转换成内存中的unicode,以及由unicode转换成其他编码的过程,都称为编码encode

    img

    由内存中的unicode转换成字符,以及由其他编码转换成unicode的过程,都称为解码decode

    img

    在诸多文件类型中,只有文本文件的内存是由字符组成的,因而文本文件的存取也涉及到字符编码的问题

    3.4 utf-8的由来

    注意:如果保存到硬盘的是GBK格式二进制,当初用户输入的字符只能是中文或英文,同理如果保存到硬盘的是Shift_JIS格式二进制,当初用户输入的字符只能是日文或英文……如果我们输入的字符中包含多国字符,那么该如何处理?

    #多国字符—√—》内存(unicode格式的二进制)——X—》硬盘(GBK格式的二进制)
    
    #多国字符—√—》内存(unicode格式的二进制)——X—》硬盘(Shift_JIS格式的二进制)
    
    #多国字符—√—》内存(unicode格式的二进制)——√—》硬盘(???格式的二进制)
    

    理论上是可以将内存中unicode格式的二进制直接存放于硬盘中的,但由于unicode固定使用两个字节来存储一个字符,如果多国字符中包含大量的英文字符时,使用unicode格式存放会额外占用一倍空间(英文字符其实只需要用一个字节存放即可),然而空间占用并不是最致命的问题,最致命地是当我们由内存写入硬盘时会额外耗费一倍的时间,所以将内存中的unicode二进制写入硬盘或者基于网络传输时必须将其转换成一种精简的格式,这种格式即utf-8(全称Unicode Transformation Format,即unicode的转换格式)

    # 多国字符—√—》内存(unicode格式的二进制)——√—》硬盘(utf-8格式的二进制)
    

    img

    那为何在内存中不直接使用utf-8呢?

    utf-8是针对Unicode的可变长度字符编码:一个英文字符占1Bytes,一个中文字符占3Bytes,生僻字用更多的Bytes存储
    
    unicode更像是一个过渡版本,我们新开发的软件或文件存入硬盘都采用utf-8格式,等过去几十年,所有老编码的文件都淘汰
    

    四 字符编码的应用

    我们学习字符编码就是为了存取字符时不发生乱码问题:

    #1、内存中固定使用unicode无论输入任何字符都不会发生乱码
    
    #2、我们能够修改的是存/取硬盘的编码方式,如果编码设置不正确将会出现乱码问题。乱码问题分为两种:存乱了,读乱了
    
    #2.1 存乱了:如果用户输入的内容中包含中文和日文字符,如果单纯以shift_JIS存,日文可以正常写入硬盘,而由于中文字符在shift_jis中没有找到对应关系而导致存乱了
    
    #2.2 读乱了:如果硬盘中的数据是shift_JIS格式存储的,采GBK格式读入内存就读乱了
    

    总结:

    #1. 保证存的时候不乱:在由内存写入硬盘时,必须将编码格式设置为支持所输入字符的编码格式
    #2. 保证存的时候不乱:在由硬盘读入内存时,必须采用与写入硬盘时相同的编码格式
    

    4.1 文本编辑器nodpad++存取文本文件

    文本编辑器存取的都是文本文件,而文本文件中包含的内容全为字符,所以存取文本文件都涉及到字符编码的问题。

    img

    4.2 python解释器执行文件的前两个阶段

    执行py文件的前两个阶段就是python解释器读文本文件的过程,与文本编辑读文本文件的前两个阶段没人任何区别,要保证读不乱码,则必须将python解释器读文件时采用的编码方式设置为文件当初写入硬盘时的编码格式,如果没有设置,python解释器则才用默认的编码方式,在python3中默认为utf-8,在python2中默认为ASCII,我们可以通过指定文件头来修改默认的编码

    • 在文件首行写入包含#号在内的以下内容
    # coding: 当初文件写入硬盘时采用的编码格式
    

    解释器会先用默认的编码方式读取文件的首行内容,由于首行是纯英文组成,而任何编码方式都可以识别英文字符。

    4.3 python解释器执行文件的第三个阶段

    设置文件头的作用是保证运行python程序的前两个阶段不乱码,经过前两个阶段后py文件的内容都会以unicode格式存放于内存中。

    在经历第三个阶段时开始识别python语法,当遇到特定的语法name = ‘上’(代码本身也都全都是unicode格式存的)时,需要申请内存空间来存储字符串’上’,这就又涉及到应该以什么编码存储‘上’的问题了。

    在Python3中,字符串类的值都是使用unicode格式来存储

    由于Python2的盛行是早于unicode的,因此在Python2中是按照文件头指定的编码来存储字符串类型的值的(如果文件头中没有指定编码,那么解释器会按照它自己默认的编码方式来存储‘上’),所以,这就有可能导致乱码问题

    # coding:utf-8
    x = '上' # x的值为untf-8格式的二进制
    print(x) # 打印操作是将x的值,即utf-8格式的二进制交给终端,当终端收到后发现并不是unicode(只有unicode才与字符有对应关系),所以终端会执行操作:utf-8二进制---解码-->unicode格式的二进制,解码的过程终端会采用自己默认的编码,而在pycharm的终端默认编码为utf-8、windows下的cmd终端的默认编码为gbk,所以该打印操作在pycharm中显示正常,而在windows下的cmd中则乱码
    
    # 在windows下的cmd中运行效果如下
    C:\Users\Administrator>python2 E:\aaa.py
    涓
    

    python2后推出了一种补救措施,就是在字符串类型前加u,则会将字符串类型强制存储unicode,这就与python3保持一致了,对于unicode格式无论丢给任何终端进行打印,都可以直接对应字符不会出现乱码问题

    # coding:utf-8
    x = u'上' # 即便文件头为utf-8,x的值依然存成unicode
    

    4.4 字符串encode编码与decode解码的使用

    # 1、unicode格式------编码encode-------->其它编码格式
    >>> x='上' # 在python3在'上'被存成unicode
    >>> res=x.encode('utf-8')
    >>> res,type(res) # unicode编码成了utf-8格式,而编码的结果为bytes类型,可以当作直接当作二进制去使用
    (b'\xe4\xb8\x8a', <class 'bytes'>)
    
    # 2、其它编码格式------解码decode-------->unicode格式
    >>> res.decode('utf-8') 
    '上'
    

    10、文件处理

    应用程序运行过程中产生的数据最先都是存放于内存中的,若想永久保存下来,必须要保存于硬盘中。应用程序若想操作硬件必须通过操作系统,而文件就是操作系统提供给应用程序来操作硬盘的虚拟概念,用户或应用程序对文件的操作,就是向操作系统发起调用,然后由操作系统完成对硬盘的具体操作。

    二 文件操作的基本流程

    2.1 基本流程

    有了文件的概念,我们无需再去考虑操作硬盘的细节,只需要关注操作文件的流程:

    # 1. 打开文件,由应用程序向操作系统发起系统调用open(...),操作系统打开该文件,对应一块硬盘空间,并返回一个文件对象赋值给一个变量f
    f=open('a.txt','r',encoding='utf-8') #默认打开模式就为r
    
    # 2. 调用文件对象下的读/写方法,会被操作系统转换为读/写硬盘的操作
    data=f.read()
    
    # 3. 向操作系统发起关闭文件的请求,回收系统资源
    f.close()
    

    2.2 资源回收与with上下文管理

    打开一个文件包含两部分资源:应用程序的变量f和操作系统打开的文件。在操作完毕一个文件时,必须把与该文件的这两部分资源全部回收,回收方法为:

    1、f.close() #回收操作系统打开的文件资源
    2del f #回收应用程序级的变量
    

    其中del f一定要发生在f.close()之后,否则就会导致操作系统打开的文件无法关闭,白白占用资源, 而python自动的垃圾回收机制决定了我们无需考虑del f,这就要求我们,在操作完毕文件后,一定要记住f.close(),虽然我们如此强调,但是大多数读者还是会不由自主地忘记f.close(),考虑到这一点,python提供了with关键字来帮我们管理上下文

    # 1、在执行完子代码块后,with 会自动执行f.close()
    with open('a.txt','w') as f:
        pass 
    
    # 2、可用用with同时打开多个文件,用逗号分隔开即可
    with open('a.txt','r') as read_f,open('b.txt','w') as write_f:  
        data = read_f.read()
        write_f.write(data)
    

    2.3 指定操作文本文件的字符编码

    f = open(...)是由操作系统打开文件,如果打开的是文本文件,会涉及到字符编码问题,如果没有为open指定编码,那么打开文本文件的默认编码很明显是操作系统说了算了,操作系统会用自己的默认编码去打开文件,在windows下是gbk,在linux下是utf-8。
    这就用到了上节课讲的字符编码的知识:若要保证不乱码,文件以什么方式存的,就要以什么方式打开。
    
    f = open('a.txt','r',encoding='utf-8')
    

    三 文件的操作模式

    3.1 控制文件读写操作的模式

    r(默认的):只读
    w:只写
    a:只追加写
    

    3.1.1 案例一:r 模式的使用

    # r只读模式: 在文件不存在时则报错,文件存在文件内指针直接跳到文件开头
     with open('a.txt',mode='r',encoding='utf-8') as f:
         res=f.read() # 会将文件的内容由硬盘全部读入内存,赋值给res
    
    # 小练习:实现用户认证功能
     inp_name=input('请输入你的名字: ').strip()
     inp_pwd=input('请输入你的密码: ').strip()
     with open(r'db.txt',mode='r',encoding='utf-8') as f:
         for line in f:
             # 把用户输入的名字与密码与读出内容做比对
             u,p=line.strip('\n').split(':')
             if inp_name == u and inp_pwd == p:
                 print('登录成功')
                 break
         else:
             print('账号名或者密码错误')
    

    3.1.2 案例二:w 模式的使用

    # w只写模式: 在文件不存在时会创建空文档,文件存在会清空文件,文件指针跑到文件开头
    with open('b.txt',mode='w',encoding='utf-8') as f:
        f.write('你好\n')
        f.write('我好\n') 
        f.write('大家好\n')
        f.write('111\n222\n333\n')
    #强调:
    # 1 在文件不关闭的情况下,连续的写入,后写的内容一定跟在前写内容的后面
    # 2 如果重新以w模式打开文件,则会清空文件内容
    

    3.1.3 案例三:a 模式的使用

    # a只追加写模式: 在文件不存在时会创建空文档,文件存在会将文件指针直接移动到文件末尾
     with open('c.txt',mode='a',encoding='utf-8') as f:
         f.write('44444\n')
         f.write('55555\n')
    #强调 w 模式与 a 模式的异同:
    # 1 相同点:在打开的文件不关闭的情况下,连续的写入,新写的内容总会跟在前写的内容之后
    # 2 不同点:以 a 模式重新打开文件,不会清空原文件内容,会将文件指针直接移动到文件末尾,新写的内容永远写在最后
    
    # 小练习:实现注册功能:
     name=input('username>>>: ').strip()
     pwd=input('password>>>: ').strip()
     with open('db1.txt',mode='a',encoding='utf-8') as f:
         info='%s:%s\n' %(name,pwd)
         f.write(info)
    

    3.1.4 案例四:+ 模式的使用(了解)

    # r+ w+ a+ :可读可写
    #在平时工作中,我们只单纯使用r/w/a,要么只读,要么只写,一般不用可读可写的模式
    

    3.2 控制文件读写内容的模式

    大前提: tb模式均不能单独使用,必须与r/w/a之一结合使用
    t(默认的):文本模式
        1. 读写文件都是以字符串为单位的
        2. 只能针对文本文件
        3. 必须指定encoding参数
    b:二进制模式:
       1.读写文件都是以bytes/二进制为单位的
       2. 可以针对所有文件
       3. 一定不能指定encoding参数
    

    3.2.1 案例一:t 模式的使用

    # t 模式:如果我们指定的文件打开模式为r/w/a,其实默认就是rt/wt/at
     with open('a.txt',mode='rt',encoding='utf-8') as f:
         res=f.read() 
         print(type(res)) # 输出结果为:<class 'str'>
    
     with open('a.txt',mode='wt',encoding='utf-8') as f:
         s='abc'
         f.write(s) # 写入的也必须是字符串类型
    
     #强调:t 模式只能用于操作文本文件,无论读写,都应该以字符串为单位,而存取硬盘本质都是二进制的形式,当指定 t 模式时,内部帮我们做了编码与解码
    

    3.2.2 案例二: b 模式的使用

    # b: 读写都是以二进制位单位
     with open('1.mp4',mode='rb') as f:
         data=f.read()
         print(type(data)) # 输出结果为:<class 'bytes'>
    
     with open('a.txt',mode='wb') as f:
         msg="你好"
         res=msg.encode('utf-8') # res为bytes类型
         f.write(res) # 在b模式下写入文件的只能是bytes类型
    
    #强调:b模式对比t模式
    1、在操作纯文本文件方面t模式帮我们省去了编码与解码的环节,b模式则需要手动编码与解码,所以此时t模式更为方便
    2、针对非文本文件(如图片、视频、音频等)只能使用b模式
    
    # 小练习: 编写拷贝工具
    src_file=input('源文件路径: ').strip()
    dst_file=input('目标文件路径: ').strip()
    with open(r'%s' %src_file,mode='rb') as read_f,open(r'%s' %dst_file,mode='wb') as write_f:
        for line in read_f:
            # print(line)
            write_f.write(line)
    

    四 操作文件的方法

    4.1 重点

    # 读操作
    f.read()  # 读取所有内容,执行完该操作后,文件指针会移动到文件末尾
    f.readline()  # 读取一行内容,光标移动到第二行首部
    f.readlines()  # 读取每一行内容,存放于列表中
    
    # 强调:
    # f.read()与f.readlines()都是将内容一次性读入内容,如果内容过大会导致内存溢出,若还想将内容全读入内存,则必须分多次读入,有两种实现方式:
    # 方式一
    with open('a.txt',mode='rt',encoding='utf-8') as f:
        for line in f:
            print(line) # 同一时刻只读入一行内容到内存中
    
    # 方式二
    with open('1.mp4',mode='rb') as f:
        while True:
            data=f.read(1024) # 同一时刻只读入1024个Bytes到内存中
            if len(data) == 0:
                break
            print(data)
    
    # 写操作
    f.write('1111\n222\n')  # 针对文本模式的写,需要自己写换行符
    f.write('1111\n222\n'.encode('utf-8'))  # 针对b模式的写,需要自己写换行符
    f.writelines(['333\n','444\n'])  # 文件模式
    f.writelines([bytes('333\n',encoding='utf-8'),'444\n'.encode('utf-8')]) #b模式
    

    4.2 了解

    f.readable()  # 文件是否可读
    f.writable()  # 文件是否可读
    f.closed  # 文件是否关闭
    f.encoding  # 如果文件打开模式为b,则没有该属性
    f.flush()  # 立刻将文件内容从内存刷到硬盘
    f.name
    

    五 主动控制文件内指针移动

    #大前提:文件内指针的移动都是Bytes为单位的,唯一例外的是t模式下的read(n),n以字符为单位
    with open('a.txt',mode='rt',encoding='utf-8') as f:
         data=f.read(3) # 读取3个字符
    
    
    with open('a.txt',mode='rb') as f:
         data=f.read(3) # 读取3个Bytes
    
    
    # 之前文件内指针的移动都是由读/写操作而被动触发的,若想读取文件某一特定位置的数据,则则需要用f.seek方法主动控制文件内指针的移动,详细用法如下:
    # f.seek(指针移动的字节数,模式控制): 
    # 模式控制:
    # 0: 默认的模式,该模式代表指针移动的字节数是以文件开头为参照的
    # 1: 该模式代表指针移动的字节数是以当前所在的位置为参照的
    # 2: 该模式代表指针移动的字节数是以文件末尾的位置为参照的
    # 强调:其中0模式可以在t或者b模式使用,而1跟2模式只能在b模式下用
    

    5.1 案例一: 0模式详解

    # a.txt用utf-8编码,内容如下(abc各占1个字节,中文“你好”各占3个字节)
    abc你好
    
    # 0模式的使用
    with open('a.txt',mode='rt',encoding='utf-8') as f:
        f.seek(3,0)     # 参照文件开头移动了3个字节
        print(f.tell()) # 查看当前文件指针距离文件开头的位置,输出结果为3
        print(f.read()) # 从第3个字节的位置读到文件末尾,输出结果为:你好
        # 注意:由于在t模式下,会将读取的内容自动解码,所以必须保证读取的内容是一个完整中文数据,否则解码失败
    
    with open('a.txt',mode='rb') as f:
        f.seek(6,0)
        print(f.read().decode('utf-8')) #输出结果为: 好
    

    5.2 案例二: 1模式详解

    # 1模式的使用
    with open('a.txt',mode='rb') as f:
        f.seek(3,1) # 从当前位置往后移动3个字节,而此时的当前位置就是文件开头
        print(f.tell()) # 输出结果为:3
        f.seek(4,1)     # 从当前位置往后移动4个字节,而此时的当前位置为3
        print(f.tell()) # 输出结果为:7
    

    5.3 案例三: 2模式详解

    # a.txt用utf-8编码,内容如下(abc各占1个字节,中文“你好”各占3个字节)
    abc你好
    
    # 2模式的使用
    with open('a.txt',mode='rb') as f:
        f.seek(0,2)     # 参照文件末尾移动0个字节, 即直接跳到文件末尾
        print(f.tell()) # 输出结果为:9
        f.seek(-3,2)     # 参照文件末尾往前移动了3个字节
        print(f.read().decode('utf-8')) # 输出结果为:好
    
    # 小练习:实现动态查看最新一条日志的效果
    import time
    with open('access.log',mode='rb') as f:
        f.seek(0,2)
        while True:
            line=f.readline()
            if len(line) == 0:
                # 没有内容
                time.sleep(0.5)
            else:
                print(line.decode('utf-8'),end='')
    

    六 文件的修改

    # 文件a.txt内容如下
    张一蛋     山东    179    49    12344234523
    李二蛋     河北    163    57    13913453521
    王全蛋     山西    153    62    18651433422
    
    # 执行操作
    with open('a.txt',mode='r+t',encoding='utf-8') as f:
        f.seek(9)
        f.write('<妇女主任>')
    
    # 文件修改后的内容如下
    张一蛋<妇女主任> 179    49    12344234523
    李二蛋     河北    163    57    13913453521
    王全蛋     山西    153    62    18651433422
    
    # 强调:
    # 1、硬盘空间是无法修改的,硬盘中数据的更新都是用新内容覆盖旧内容
    # 2、内存中的数据是可以修改的
    

    文件对应的是硬盘空间,硬盘不能修改对应着文件本质也不能修改, 那我们看到文件的内容可以修改,是如何实现的呢? 大致的思路是将硬盘中文件内容读入内存,然后在内存中修改完毕后再覆盖回硬盘 具体的实现方式分为两种:

    6.1 文件修改方式一

    # 实现思路:将文件内容发一次性全部读入内存,然后在内存中修改完毕后再覆盖写回原文件
    # 优点: 在文件修改过程中同一份数据只有一份
    # 缺点: 会过多地占用内存
    with open('db.txt',mode='rt',encoding='utf-8') as f:
        data=f.read()
    
    with open('db.txt',mode='wt',encoding='utf-8') as f:
        f.write(data.replace('kevin','SB'))
    

    6.1 文件修改方式二

    # 实现思路:以读的方式打开原文件,以写的方式打开一个临时文件,一行行读取原文件内容,修改完后写入临时文件...,删掉原文件,将临时文件重命名原文件名
    # 优点: 不会占用过多的内存
    # 缺点: 在文件修改过程中同一份数据存了两份
    import os
    
    with open('db.txt',mode='rt',encoding='utf-8') as read_f,\
            open('.db.txt.swap',mode='wt',encoding='utf-8') as wrife_f:
        for line in read_f:
            wrife_f.write(line.replace('SB','kevin'))
    
    os.remove('db.txt')
    os.rename('.db.txt.swap','db.txt')
    

    11、函数的基本使用

    一 引入

    在程序中,具备某一功能的‘工具’指的就是函数,‘事先准备工具’的过程即函数的定义,‘拿来就用’即函数的调用。

    二 定义函数

    函数的使用必须遵循’先定义,后调用’的原则。函数的定义就相当于事先将函数体代码保存起来,然后将内存地址赋值给函数名,函数名就是对这段代码的引用,这和变量的定义是相似的。没有事先定义函数而直接调用,就相当于在引用一个不存在的’变量名’。

    定义函数的语法

    def 函数名(参数1,参数2,...):
        """文档描述"""
        函数体
        return 值
    
    1. def: 定义函数的关键字;
    2. 函数名:函数名指向函数内存地址,是对函数体代码的引用。函数的命名应该反映出函数的功能;
    3. 括号:括号内定义参数,参数是可有可无的,且无需指定参数的类型;
    4. 冒号:括号后要加冒号,然后在下一行开始缩进编写函数体的代码;
    5. “”“文档描述”"": 描述函数功能,参数介绍等信息的文档,非必要,但是建议加上,从而增强函数的可读性;
    6. 函数体:由语句和表达式组成;
    7. return 值:定义函数的返回值,return是可有可无的。

    参数是函数的调用者向函数体传值的媒介,若函数体代码逻辑依赖外部传来的参数时则需要定义为参函数,

    def my_min(x,y):
        res=x if x < y else y
        return res
    

    否则定义为无参函数

    def interactive():
        user=input('user>>: ').strip()
        pwd=input('password>>: ').strip()
        return (user,pwd)
    

    函数体为pass代表什么都不做,称之为空函数。定义空函数通常是有用的,因为在程序设计的开始,往往是先想好程序都需要完成什么功能,然后把所有功能都列举出来用pass充当函数体“占位符”,这将使得程序的体系结构立见,清晰且可读性强。例如要编写一个ftp程序,我们可能想到的功能有用户认证,下载,上传,浏览,切换目录等功能,可以先做出如下定义:

    def auth_user():
        """user authentication function"""
        pass
    
    def download_file():
        """download file function"""
        pass
    
    def upload_file():
        """upload file function"""
        pass
    
    def ls():
        """list contents function"""
        pass
    
    def cd():
        """change directory"""
        pass
    

    之后我们便可以统筹安排编程任务,有选择性的去实现上述功能来替换掉pass,从而提高开发效率。

    三 调用函数与函数返回值

    函数的使用分为定义阶段与调用阶段,定义函数时只检测语法,不执行函数体代码,函数名加括号即函数调用,只有调用函数时才会执行函数体代码

    #定义阶段
    def foo():
        print('in the foo')
        bar()
    
    def bar():
        print('in the bar')
    
    #调用阶段
    foo()
    

    执行结果:

    in the foo
    in the bar
    

    定义阶段函数foo与bar均无语法错误,而在调用阶段调用foo()时,函数foo与bar都早已经存在于内存中了,所以不会有任何问题。

    按照在程序出现的形式和位置,可将函数的调用形式分为三种

    #1、语句形式:
    foo()
    
    #2、表达式形式:
    m=my_min(1,2) #将调用函数的返回值赋值给x
    n=10*my_min(1,2) #将调用函数的返回值乘以10的结果赋值给n
    
    #3、函数调用作为参数的形式:
    # my_min(2,3)作为函数my_min的第二个参数,实现了取1,2,3中的较小者赋值给m
    m=my_min(1,my_min(23)
    

    若需要将函数体代码执行的结果返回给调用者,则需要用到return。return后无值或直接省略return,则默认返回None,return的返回值无类型限制,且可以将多个返回值放到一个元组内。

    >>> def test(x,y,z):
    ...     return x,y,z #等同于return (x,y,z)
    ... 
    >>> res=test(1,2,3)
    >>> print(res)
    (1, 2, 3)
    

    return是一个函数结束的标志,函数内可以有多个return,但只执行一次函数就结束了,并把return后定义的值作为本次调用的结果返回。

    12、函数的参数

    一 形参与实参介绍

    函数的参数分为形式参数和实际参数,简称形参和实参:

    形参即在定义函数时,括号内声明的参数。形参本质就是一个变量名,用来接收外部传来的值。

    实参即在调用函数时,括号内传入的值,值可以是常量、变量、表达式或三者的组合:

    #1:实参是常量
    res=my_min(1,2)
    
    #2:实参是变量
    a=1
    b=2
    res=my_min(a,b)
    
    #3:实参是表达式
    res=my_min(10*2,10*my_min(3,4))
    
    #4:实参可以是常量、变量、表达式的任意组合
    a=2
    my_min(1,a,10*my_min(3,4))
    

    在调用有参函数时,实参(值)会赋值给形参(变量名)。在Python中,变量名与值只是单纯的绑定关系,而对于函数来说,这种绑定关系只在函数调用时生效,在调用结束后解除。

    二 形参与实参的具体使用

    2.1 位置参数

    位置即顺序,位置参数指的是按顺序定义的参数,需要从两个角度去看:

    1. 在定义函数时,按照从左到右的顺序依次定义形参,称为位置形参,凡是按照这种形式定义的形参都必须被传值
    def register(name,age,sex): #定义位置形参:name,age,sex,三者都必须被传值
        print('Name:%s Age:%s Sex:%s' %(name,age,sex))
    register() #TypeError:缺少3个位置参数 
    

    在调用函数时,按照从左到右的顺序依次定义实参,称为位置实参,凡是按照这种形式定义的实参会按照从左到右的顺序与形参一一对应

    def register(name,age,sex): #定义位置形参:name,age,sex,三者都必须被传值
        print('Name:%s Age:%s Sex:%s' %(name,age,sex))
    register() #TypeError:缺少3个位置参数
    

    2.2 关键字参数

    在调用函数时,实参可以是key=value的形式,称为关键字参数,凡是按照这种形式定义的实参,可以完全不按照从左到右的顺序定义,但仍能为指定的形参赋值

    >>> register(sex='male',name='lili',age=18)
    Name:lili Age:18 Sex:male
    

    需要注意在调用函数时,实参也可以是按位置或按关键字的混合使用,但必须保证关键字参数在位置参数后面,且不可以对一个形参重复赋值

    >>> register('lili',sex='male',age=18) #正确使用
    >>> register(name='lili',18,sex='male') #SyntaxError:关键字参数name=‘lili’在位置参数18之前
    >>> register('lili',sex='male',age=18,name='jack') #TypeError:形参name被重复赋值
    

    2.3 默认参数

    在定义函数时,就已经为形参赋值,这类形参称之为默认参数,当函数有多个参数时,需要将值经常改变的参数定义成位置参数,而将值改变较少的参数定义成默认参数。例如编写一个注册学生信息的函数,如果大多数学生的性别都为男,那完全可以将形参sex定义成默认参数

    >>> def register(name,age,sex='male'): #默认sex的值为male
    ...     print('Name:%s Age:%s Sex:%s' %(name,age,sex))
    ...
    

    定义时就已经为参数sex赋值,意味着调用时可以不对sex赋值,这降低了函数调用的复杂度

    >>> register('tom',17) #大多数情况,无需为sex传值,默认为male
    Name:tom Age:17 Sex:male
    >>> register('Lili',18,'female') #少数情况,可以为sex传值female
    Name:Lili Age:18 Sex:female
    

    需要注意:

    1. 默认参数必须在位置参数之后
    2. 默认参数的值仅在函数定义阶段被赋值一次
    >>> x=1
    >>> def foo(arg=x):
    ...     print(arg)
    ... 
    >>> x=5 #定义阶段arg已被赋值为1,此处的修改与默认参数arg无任何关系
    >>> foo()
    1
    
    1. 默认参数的值通常应设为不可变类型
    def foo(n,arg=[]):    
         arg.append(n)    
         return arg    
    foo(1)    
    [1] 
    foo(2)    
    [1, 2] 
    foo(3)    
    [1, 2, 3]
    

    每次调用是在上一次的基础上向同一列表增加值,修改如下

    def foo(n,arg=None):    
         if arg is None:    
             arg=[]    
         arg.append(n)    
         return arg    
    foo(1)    
    [1] 
    foo(2)    
    [2] 
    foo(3)    
    [3]
    

    2.4 可变长度的参数(*与**的用法)

    参数的长度可变指的是在调用函数时,实参的个数可以不固定,而在调用函数时,实参的定义无非是按位置或者按关键字两种形式,这就要求形参提供两种解决方案来分别处理两种形式的可变长度的参数

    2.4.1 可变长度的位置参数

    如果在最后一个形参名前加号,那么在调用函数时,溢出的位置实参,都会被接收,以元组的形式保存下来赋值给该形参

    >>> def foo(x,y,z=1,*args): #在最后一个形参名args前加*号
    ...     print(x)
    ...     print(y)
    ...     print(z)
    ...     print(args)
    ... 
    >>> foo(1,2,3,4,5,6,7)  #实参1、2、3按位置为形参x、y、z赋值,多余的位置实参4、5、6、7都被*接收,以元组的形式保存下来,赋值给args,即args=(4, 5, 6,7)
    
    1
    2
    3
    (4, 5, 6, 7)
    

    如果我们事先生成了一个列表,仍然是可以传值给*args的

    >>> def foo(x,y,*args):
    ...     print(x)
    ...     print(y)
    ...     print(args)
    ... 
    >>> L=[3,4,5]
    >>> foo(1,2,*L) # *L就相当于位置参数3,4,5, foo(1,2,*L)就等同于foo(1,2,3,4,5)
    1
    2
    (3, 4, 5)
    

    注意:如果在传入L时没有加*,那L就只是一个普通的位置参数了

    >>> foo(1,2,L) #仅多出一个位置实参L
    1
    2
    ([1, 2, 3],)
    

    如果形参为常规的参数(位置或默认),实参仍可以是*的形式

    >>> def foo(x,y,z=3):
    ...     print(x)
    ...     print(y)
    ...     print(z)
    ... 
    >>> foo(*[1,2]) #等同于foo(1,2)
    1
    2
    3
    

    如果我们想要求多个值的和,*args就派上用场了

    >>> def add(*args):
    ...     res=0
    ...     for i in args:
    ...         res+=i
    ...     return res
    ... 
    >>> add(1,2,3,4,5)
    15
    

    2.4.2 可变长度的关键字参数

    如果在最后一个形参名前加号,那么在调用函数时,溢出的关键字参数,都会被接收,以字典的形式保存下来赋值给该形参

    >>> def foo(x,**kwargs): #在最后一个参数kwargs前加**
    ...     print(x)        
    ...     print(kwargs)   
    ... 
    >>> foo(y=2,x=1,z=3) #溢出的关键字实参y=2,z=3都被**接收,以字典的形式保存下来,赋值给kwargs
    1
    {'z': 3, 'y': 2}
    

    如果我们事先生成了一个字典,仍然是可以传值给**kwargs的

    >>> def foo(x,y,**kwargs):
    ...     print(x)
    ...     print(y)
    ...     print(kwargs)
    ... 
    >>> dic={'a':1,'b':2} 
    >>> foo(1,2,**dic) #**dic就相当于关键字参数a=1,b=2,foo(1,2,**dic)等同foo(1,2,a=1,b=2)
    1
    2
    {'a': 1, 'b': 2}
    

    注意:如果在传入dic时没有加**,那dic就只是一个普通的位置参数了

    >>> foo(1,2,dic) #TypeError:函数foo只需要2个位置参数,但是传了3个
    

    如果形参为常规参数(位置或默认),实参仍可以是**的形式

    >>> def foo(x,y,z=3):
    ...     print(x)
    ...     print(y)
    ...     print(z)
    ... 
    >>> foo(**{'x':1,'y':2}) #等同于foo(y=2,x=1)
    1
    2
    3
    

    如果我们要编写一个用户认证的函数,起初可能只基于用户名密码的验证就可以了,可以使用**kwargs为日后的扩展供良好的环境,同时保持了函数的简洁性。

    >>> def auth(user,password,**kwargs): 
    ...     pass 
    ...
    

    2.5 命名关键字参数

    在定义了**kwargs参数后,函数调用者就可以传入任意的关键字参数key=value,如果函数体代码的执行需要依赖某个key,必须在函数内进行判断

    >>> def register(name,age,**kwargs):
    ...     if 'sex' in kwargs:
    ...         #有sex参数
    ...         pass
    ...     if 'height' in kwargs:
    ...         #有height参数
    ...         pass
    ...
    

    想要限定函数的调用者必须以key=value的形式传值,Python3提供了专门的语法:需要在定义形参时,用*作为一个分隔符号,*号之后的形参称为命名关键字参数。对于这类参数,在函数调用时,必须按照key=value的形式为其传值,且必须被传值

    >>> def register(name,age,*,sex,height): #sex,height为命名关键字参数
    ...     pass
    ... 
    >>> register('lili',18,sex='male',height='1.8m') #正确使用
    >>> register('lili',18,'male','1.8m') # TypeError:未使用关键字的形式为sex和height传值
    >>> register('lili',18,height='1.8m') # TypeError没有为命名关键字参数height传值。
    

    命名关键字参数也可以有默认值,从而简化调用

    >>> def register(name,age,*,sex='male',height):
    ...     print('Name:%s,Age:%s,Sex:%s,Height:%s' %(name,age,sex,height))
    ... 
    >>> register('lili',18,height='1.8m')
    Name:lili,Age:18,Sex:male,Height:1.8m
    

    需要强调的是:sex不是默认参数,height也不是位置参数,因为二者均在后,所以都是命名关键字参数,形参sex=’male’属于命名关键字参数的默认值,因而即便是放到形参height之前也不会有问题。另外,如果形参中已经有一个args了,命名关键字参数就不再需要一个单独的*作为分隔符号了

    >>> def register(name,age,*args,sex='male',height):
    ...   print('Name:%s,Age:%s,Args:%s,Sex:%s,Height:%s' %(name,age,args,sex,height))
    ... 
    >>> register('lili',18,1,2,3,height='1.8m') #sex与height仍为命名关键字参数
    Name:lili,Age:18,Args:(1, 2, 3),Sex:male,Height:1.8m
    

    2.6 组合使用

    综上所述所有参数可任意组合使用,但定义顺序必须是:位置参数、默认参数、*args、命名关键字参数、**kwargs

    可变参数*args与关键字参数kwargs通常是组合在一起使用的,如果一个函数的形参为*args与kwargs,那么代表该函数可以接收任何形式、任意长度的参数

    >>> def wrapper(*args,**kwargs):
    ...     pass
    ...
    

    在该函数内部还可以把接收到的参数传给另外一个函数(这在4.6小节装饰器的实现中大有用处)

    >>> def func(x,y,z):
    ...     print(x,y,z)
    ... 
    >>> def wrapper(*args,**kwargs):
    ...     func(*args,**kwargs)
    ...
    >>> wrapper(1,z=3,y=2)
    1 2 3
    

    按照上述写法,在为函数wrapper传参时,其实遵循的是函数func的参数规则,调用函数wrapper的过程分析如下:

    1. 位置实参1被*接收,以元组的形式保存下来,赋值给args,即args=(1,),关键字实参z=3,y=2被**接收,以字典的形式保存下来,赋值给kwargs,即kwargs={‘y’: 2, ‘z’: 3}
    2. 执行func(args,kwargs),即func((1,),* {‘y’: 2, ‘z’: 3}),等同于func(1,z=3,y=2)
    提示: *args、**kwargs中的args和kwargs被替换成其他名字并无语法错误,但使用args、kwargs是约定
    

    13、名称空间与作用域

    一 名称空间

    名称空间即存放名字与对象映射/绑定关系的地方。对于x=3,Python会申请内存空间存放对象3,然后将名字x与3的绑定关系存放于名称空间中,del x表示清除该绑定关系。

    在程序执行期间最多会存在三种名称空间

    1.1 内建名称空间

    伴随python解释器的启动/关闭而产生/回收,因而是第一个被加载的名称空间,用来存放一些内置的名字,比如内建函数名

    >>> max
    <built-in function max> #built-in内建
    

    1.2 全局名称空间

    伴随python文件的开始执行/执行完毕而产生/回收,是第二个被加载的名称空间,文件执行过程中产生的名字都会存放于该名称空间中,如下名字

    import sys #模块名sys
    
    x=1 #变量名x
    
    if x == 1:
        y=2 #变量名y
    
    def foo(x): #函数名foo
        y=1
        def bar():
            pass
    
    Class Bar: #类名Bar
        pass
    

    1.3 局部名称空间

    伴随函数的调用/结束而临时产生/回收,函数的形参、函数内定义的名字都会被存放于该名称空间中

    def foo(x):
        y=3 #调用函数时,才会执行函数代码,名字x和y都存放于该函数的局部名称空间中
    

    名称空间的加载顺序是:内置名称空间->全局名称空间->局部名称空间,而查找一个名字,必须从三个名称空间之一找到,查找顺序为:局部名称空间->全局名称空间->内置名称空间。

    二 作用域

    2.1 全局作用域与局部作用域

    按照名字作用范围的不同可以将三个名称空间划分为两个区域:

    1. 全局作用域:位于全局名称空间、内建名称空间中的名字属于全局范围,该范围内的名字全局存活(除非被删除,否则在整个文件执行过程中存活)、全局有效(在任意位置都可以使用);
    2. 局部作用域:位于局部名称空间中的名字属于局部范围。该范围内的名字临时存活(即在函数调用时临时生成,函数调用结束后就释放)、局部有效(只能在函数内使用)。

    2.2 作用域与名字查找的优先级

    在局部作用域查找名字时,起始位置是局部作用域,所以先查找局部名称空间,没有找到,再去全局作用域查找:先查找全局名称空间,没有找到,再查找内置名称空间,最后都没有找到就会抛出异常

    x=100 #全局作用域的名字x
    def foo():
        x=300 #局部作用域的名字x
        print(x) #在局部找x
    foo()#结果为300
    

    在全局作用域查找名字时,起始位置便是全局作用域,所以先查找全局名称空间,没有找到,再查找内置名称空间,最后都没有找到就会抛出异常

    x=100
    def foo():
        x=300 #在函数调用时产生局部作用域的名字x
    foo()
    print(x) #在全局找x,结果为100
    

    提示:可以调用内建函数locals()和globals()来分别查看局部作用域和全局作用域的名字,查看的结果都是字典格式。在全局作用域查看到的locals()的结果等于globals()

    Python支持函数的嵌套定义,在内嵌的函数内查找名字时,会优先查找自己局部作用域的名字,然后由内而外一层层查找外部嵌套函数定义的作用域,没有找到,则查找全局作用域

    x=1
    def outer():
        x=2
        def inner(): # 函数名inner属于outer这一层作用域的名字
            x=3
            print('inner x:%s' %x)
    
        inner()
        print('outer x:%s' %x)
    
    outer() 
    #结果为
    inner x:3
    outer x:2
    

    在函数内,无论嵌套多少层,都可以查看到全局作用域的名字,若要在函数内修改全局名称空间中名字的值,当值为不可变类型时,则需要用到global关键字

    x=1
    def outer():
        x=2
        def inner(): # 函数名inner属于outer这一层作用域的名字
            x=3
            print('inner x:%s' %x)
    
        inner()
        print('outer x:%s' %x)
    
    outer() 
    #结果为
    inner x:3
    outer x:2
    

    在函数内,无论嵌套多少层,都可以查看到全局作用域的名字,若要在函数内修改全局名称空间中名字的值,当值为不可变类型时,则需要用到global关键字

    x=1
    def foo():
        global x #声明x为全局名称空间的名字
        x=2
    foo()
    print(x) #结果为2
    

    当实参的值为可变类型时,函数体内对该值的修改将直接反应到原值,

    num_list=[1,2,3]
    def foo(nums):
        nums.append(5)
    
    foo(num_list)
    print(num_list)
    #结果为
    [1, 2, 3, 5]
    

    对于嵌套多层的函数,使用nonlocal关键字可以将名字声明为来自外部嵌套函数定义的作用域(非全局)

    def  f1():
        x=2
        def f2():
            nonlocal x
            x=3
        f2() #调用f2(),修改f1作用域中名字x的值
        print(x) #在f1作用域查看x
    
    f1()
    
    #结果
    3
    

    nonlocal x会从当前函数的外层函数开始一层层去查找名字x,若是一直到最外层函数都找不到,则会抛出异常。

    14、函数对象和闭包

    一 函数对象

    函数对象指的是函数可以被当做’数据’来处理,具体可以分为四个方面的使用,我们如下

    1.1 函数可以被引用

    >>> def add(x,y):
    ...     return x+y
    ... 
    >>> func=add
    >>> func(1,2)
    3
    

    1.2 函数可以作为容器类型的元素

    >>> dic={'add':add,'max':max}
    >>> dic
    {'add': <function add at 0x100661e18>, 'max': <built-in function max>}
    >>> dic['add'](1,2)
    3
    

    1.3 函数可以作为参数传入另外一个函数

    >>> def foo(x,y,func):
    ...     return func(x,y)
    ...
    >>> foo(1,2,add)
    3
    

    1.4 函数的返回值可以是一个函数

    def bar(): 
         return add 
    func=bar() 
    func(1,2)
    3 
    

    二 闭包函数

    2.1 闭与包

    基于函数对象的概念,可以将函数返回到任意位置去调用,但作用域的关系是在定义完函数时就已经被确定了的,与函数的调用位置无关。

    x=1
    
    def f1():
        def f2():
            print(x)
    
        return f2
    
    def f3():
        x=3
        f2=f1() #调用f1()返回函数f2
        f2() #需要按照函数定义时的作用关系去执行,与调用位置无关
    
    f3() #结果为1
    

    也就是说函数被当做数据处理时,始终以自带的作用域为准。若内嵌函数包含对外部函数作用域(而非全局作用域)中变量的引用,那么该’内嵌函数’就是闭包函数,简称闭包(Closures)

    x=1
    def outer():
        x=2
        def inner():
            print(x)
        return inner
    
    func=outer()
    func() # 结果为2
    

    可以通过函数的closure属性,查看到闭包函数所包裹的外部变量

    >>> func.__closure__
    (<cell at 0x10212af78: int object at 0x10028cca0>,)
    >>> func.__closure__[0].cell_contents
    2
    

    “闭”代表函数是内部的,“包”代表函数外’包裹’着对外层作用域的引用。因而无论在何处调用闭包函数,使用的仍然是包裹在其外层的变量。

    2.2 闭包的用途

    目前为止,我们得到了两种为函数体传值的方式,一种是直接将值以参数的形式传入,另外一种就是将值包给函数

    import requests
    
    #方式一:
    def get(url):
        return requests.get(url).text
    
    #方式二:
    def page(url):
        def get():
            return requests.get(url).text
        return get
    

    提示:requests模块是用来模拟浏览器向网站发送请求并将页面内容下载到本地,需要事先安装:pip3 install requests

    对比两种方式,方式一在下载同一页面时需要重复传入url,而方式二只需要传一次值,就会得到一个包含指定url的闭包函数,以后调用该闭包函数无需再传url

    # 方式一下载同一页面
    get('https://www.python.org')
    get('https://www.python.org')
    get('https://www.python.org')
    ……
    
    # 方式二下载同一页面
    python=page('https://www.python.org')
    python()
    python()
    python()
    ……
    

    闭包函数的这种特性有时又称为惰性计算。使用将值包给函数的方式,在接下来的装饰器中也将大有用处

    15、装饰器

    一 装饰器介绍

    1.1 为何要用装饰器

    软件的设计应该遵循开放封闭原则,即对扩展是开放的,而对修改是封闭的。对扩展开放,意味着有新的需求或变化时,可以对现有代码进行扩展,以适应新的情况。对修改封闭,意味着对象一旦设计完成,就可以独立完成其工作,而不要对其进行修改。

    软件包含的所有功能的源代码以及调用方式,都应该避免修改,否则一旦改错,则极有可能产生连锁反应,最终导致程序崩溃,而对于上线后的软件,新需求或者变化又层出不穷,我们必须为程序提供扩展的可能性,这就用到了装饰器。

    1.2 什么是装饰器

    ’装饰’代指为被装饰对象添加新的功能,’器’代指器具/工具,装饰器与被装饰的对象均可以是任意可调用对象。概括地讲,装饰器的作用就是在不修改被装饰对象源代码和调用方式的前提下为被装饰对象添加额外的功能。装饰器经常用于有切面需求的场景,比如:插入日志、性能测试、事务处理、缓存、权限校验等应用场景,装饰器是解决这类问题的绝佳设计,有了装饰器,就可以抽离出大量与函数功能本身无关的雷同代码并继续重用。

    提示:可调用对象有函数,方法或者类,此处我们单以本章主题函数为例,来介绍函数装饰器,并且被装饰的对象也是函数。

    二 装饰器的实现

    函数装饰器分为:无参装饰器和有参装饰两种,二者的实现原理一样,都是’函数嵌套+闭包+函数对象’的组合使用的产物。

    2.1 无参装饰器的实现

    如果想为下述函数添加统计其执行时间的功能

    import time
    
    def index():
        time.sleep(3)
        print('Welcome to the index page’)
        return 200
    
    index() #函数执行
    

    遵循不修改被装饰对象源代码的原则,我们想到的解决方法可能是这样

    start_time=time.time()
    index() #函数执行
    stop_time=time.time()
    print('run time is %s' %(stop_time-start_time))
    

    考虑到还有可能要统计其他函数的执行时间,于是我们将其做成一个单独的工具,函数体需要外部传入被装饰的函数从而进行调用,我们可以使用参数的形式传入

    def wrapper(func): # 通过参数接收外部的值
        start_time=time.time()
        res=func()
        stop_time=time.time()
        print('run time is %s' %(stop_time-start_time))
        return res
    

    但之后函数的调用方式都需要统一改成

    wrapper(index)
    wrapper(其他函数)
    

    这便违反了不能修改被装饰对象调用方式的原则,于是我们换一种为函数体传值的方式,即将值包给函数,如下

    def timer(func):
        def wrapper(): # 引用外部作用域的变量func
            start_time=time.time()
            res=func()
            stop_time=time.time()
            print('run time is %s' %(stop_time-start_time))
            return res
        return wrapper
    

    这样我们便可以在不修改被装饰函数源代码和调用方式的前提下为其加上统计时间的功能,只不过需要事先执行一次timer将被装饰的函数传入,返回一个闭包函数wrapper重新赋值给变量名 /函数名index,如下

    index=timer(index)  #得到index=wrapper,wrapper携带对外作用域的引用:func=原始的index
    index() # 执行的是wrapper(),在wrapper的函数体内再执行最原始的index
    

    至此我们便实现了一个无参装饰器timer,可以在不修改被装饰对象index源代码和调用方式的前提下为其加上新功能。但我们忽略了若被装饰的函数是一个有参函数,便会抛出异常

    def home(name):
        time.sleep(5)
        print('Welcome to the home page',name)
    
    home=timer(home)
    home('egon')
    #抛出异常
    TypeError: wrapper() takes 0 positional arguments but 1 was given
    

    之所以会抛出异常,是因为home(‘egon’)调用的其实是wrapper(‘egon’),而函数wrapper没有参数。wrapper函数接收的参数其实是给最原始的func用的,为了能满足被装饰函数参数的所有情况,便用上*args+**kwargs组合(见4.3小节),于是修正装饰器timer如下

    def timer(func):
        def wrapper(*args,**kwargs):
            start_time=time.time()
            res=func(*args,**kwargs)
            stop_time=time.time()
            print('run time is %s' %(stop_time-start_time))
            return res
        return wrapper
    

    此时我们就可以用timer来装饰带参数或不带参数的函数了,但是为了简洁而优雅地使用装饰器,Python提供了专门的装饰器语法来取代index=timer(index)的形式,需要在被装饰对象的正上方单独一行添加@timer,当解释器解释到@timer时就会调用timer函数,且把它正下方的函数名当做实参传入,然后将返回的结果重新赋值给原函数名

    @timer # index=timer(index)
    def index():
        time.sleep(3)
        print('Welcome to the index page')
        return 200
    @timer # index=timer(home)
              def home(name):
        time.sleep(5)
        print('Welcome to the home page’,name)
    

    如果我们有多个装饰器,可以叠加多个

    @deco3
    @deco2
    @deco1
    def index():
        pass
    

    叠加多个装饰器也无特殊之处,上述代码语义如下:

    index=deco3(deco2(deco1(index)))
    

    2.2 有参装饰器的实现

    了解无参装饰器的实现原理后,我们可以再实现一个用来为被装饰对象添加认证功能的装饰器,实现的基本形式如下

    def deco(func):
        def wrapper(*args,**kwargs):
            编写基于文件的认证,认证通过则执行res=func(*args,**kwargs),并返回res
        return wrapper
    

    如果我们想提供多种不同的认证方式以供选择,单从wrapper函数的实现角度改写如下

    def deco(func):
            def wrapper(*args,**kwargs):
                if driver == 'file':
                    编写基于文件的认证,认证通过则执行res=func(*args,**kwargs),并返回res
                elif driver == 'mysql':
                    编写基于mysql认证,认证通过则执行res=func(*args,**kwargs),并返回res
            return wrapper
    

    函数wrapper需要一个driver参数,而函数deco与wrapper的参数都有其特定的功能,不能用来接受其他类别的参数,可以在deco的外部再包一层函数auth,用来专门接受额外的参数,这样便保证了在auth函数内无论多少层都可以引用到

    def auth(driver):
        def deco(func):
            ……
        return deco
    

    此时我们就实现了一个有参装饰器,使用方式如下

    先调用auth_type(driver='file'),得到@deco,deco是一个闭包函数,
    包含了对外部作用域名字driver的引用,@deco的语法意义与无参装饰器一样
    @auth(driver='file') 
    def index():     
        pass
    @auth(driver='mysql') 
    def home():
        pass  
    

    可以使用help(函数名)来查看函数的文档注释,本质就是查看函数的doc属性,但对于被装饰之后的函数,查看文档注释

    @timer
    def home(name):
        '''
        home page function
        :param name: str
        :return: None
        '''
        time.sleep(5)
        print('Welcome to the home page',name)
    
    print(help(home))
    '''
    打印结果:
    
    Help on function wrapper in module __main__:
    
    wrapper(*args, **kwargs)
    
    None
    

    在被装饰之后home=wrapper,查看home.name也可以发现home的函数名确实是wrapper,想要保留原函数的文档和函数名属性,需要修正装饰器

    def timer(func):
        def wrapper(*args,**kwargs):
            start_time=time.time()
            res=func(*args,**kwargs)
            stop_time=time.time()
            print('run time is %s' %(stop_time-start_time))
            return res
        wrapper.__doc__=func.__doc__
        wrapper.__name__=func.__name__
        return wrapper
    

    按照上述方式来实现保留原函数属性过于麻烦,functools模块下提供一个装饰器wraps专门用来帮我们实现这件事,用法如下

    from functools import wraps
    
    def timer(func):
        @wraps(func)
        def wrapper(*args,**kwargs):
            start_time=time.time()
            res=func(*args,**kwargs)
            stop_time=time.time()
            print('run time is %s' %(stop_time-start_time))
            return res
        return wrapper
    
    标准版装饰器:
    def wrapper(func):
        def inner(*args,**kwargs):
            """执行被装饰函数前的操作"""
            func(*args,**kwargs)
            """执行被装饰函数后的操作"""
        return inner
    @wrapper
    def index():
        print("is index")
    index()
    
    语法糖要接受的变量就是语法糖下面的函数名、参数就是语法糖下面的函数名、调用的方式就是看调用的哪个语法糖
    def func(args):
        print("新加了一个功能")
        return args
    @func   #index = func(index)
    def index():
        print(2)
    index()
    结果:  新加了一个功能
    

    16、迭代器

    一 迭代器介绍

    迭代器即用来迭代取值的工具,而迭代是重复反馈过程的活动,其目的通常是为了逼近所需的目标或结果,每一次对过程的重复称为一次“迭代”,而每一次迭代得到的结果会作为下一次迭代的初始值,单纯的重复并不是迭代

    while True:
        msg = input('>>: ').strip()
        print(msg)
    

    下述while循环才是一个迭代过程,不仅满足重复,而且以每次重新赋值后的index值作为下一次循环中新的索引进行取值,反复迭代,最终可以取尽列表中的值

    goods=['mac','lenovo','acer','dell','sony']
    
    index=0
    while index < len(goods):
        print(goods[index])
        index+=1
    

    1.1 可迭代对象

    通过索引的方式进行迭代取值,实现简单,但仅适用于序列类型:字符串,列表,元组。对于没有索引的字典、集合等非序列类型,必须找到一种不依赖索引来进行迭代取值的方式,这就用到了迭代器。

    要想了解迭代器为何物,必须事先搞清楚一个很重要的概念:可迭代对象(Iterable)。从语法形式上讲,内置有__iter__方法的对象都是可迭代对象,字符串、列表、元组、字典、集合、打开的文件都是可迭代对象:

    {'name':'egon'}.__iter__
    {7,8,9}.__iter__
    ……
    

    1.2 迭代器对象

    调用obj.iter()方法返回的结果就是一个迭代器对象(Iterator)。迭代器对象是内置有iternext方法的对象,打开的文件本身就是一个迭代器对象,执行迭代器对象.iter()方法得到的仍然是迭代器本身,而执行迭代器.next()方法就会计算出迭代器中的下一个值。 迭代器是Python提供的一种统一的、不依赖于索引的迭代取值方式,只要存在多个“值”,无论序列类型还是非序列类型都可以按照迭代器的方式取值

    >>> s={1,2,3} # 可迭代对象s
    >>> i=iter(s)  # 本质就是在调用s.__iter__(),返回s的迭代器对象i,
    >>> next(i) # 本质就是在调用i.__next__()
    1
    >>> next(i)
    2
    >>> next(i)
    3
    >>> next(i)  #抛出StopIteration的异常,代表无值可取,迭代结束
    

    二 for循环原理

    有了迭代器后,我们便可以不依赖索引迭代取值了,使用while循环的实现方式如下

    goods=['mac','lenovo','acer','dell','sony']
    i=iter(goods) #每次都需要重新获取一个迭代器对象
    while True:
        try:
            print(next(i))
        except StopIteration: #捕捉异常终止循环
            break
    

    for循环又称为迭代循环,in后可以跟任意可迭代对象,上述while循环可以简写为

    goods=['mac','lenovo','acer','dell','sony']
    for item in goods:   
        print(item)
    

    for 循环在工作时,首先会调用可迭代对象goods内置的iter方法拿到一个迭代器对象,然后再调用该迭代器对象的next方法将取到的值赋给item,执行循环体完成一次循环,周而复始,直到捕捉StopIteration异常,结束迭代。

    三 迭代器的优缺点

    基于索引的迭代取值,所有迭代的状态都保存在了索引中,而基于迭代器实现迭代的方式不再需要索引,所有迭代的状态就保存在迭代器中,然而这种处理方式优点与缺点并存:

    3.1 优点:

    1、为序列和非序列类型提供了一种统一的迭代取值方式。

    2、惰性计算:迭代器对象表示的是一个数据流,可以只在需要时才去调用next来计算出一个值,就迭代器本身来说,同一时刻在内存中只有一个值,因而可以存放无限大的数据流,而对于其他容器类型,如列表,需要把所有的元素都存放于内存中,受内存大小的限制,可以存放的值的个数是有限的。

    3.2 缺点:

    1、除非取尽,否则无法获取迭代器的长度

    2、只能取下一个值,不能回到开始,更像是‘一次性的’,迭代器产生后的唯一目标就是重复执行next方法直到值取尽,否则就会停留在某个位置,等待下一次调用next;若是要再次迭代同个对象,你只能重新调用iter方法去创建一个新的迭代器对象,如果有两个或者多个循环使用同一个迭代器,必然只会有一个循环能取到值。

    17、生成器

    一 生成器与yield

    若函数体包含yield关键字,再调用函数,并不会执行函数体代码,得到的返回值即生成器对象

    >>> def my_range(start,stop,step=1):
    ...     print('start...')
    ...     while start < stop:
    ...         yield start
    ...         start+=step
    ...     print('end...')
    ... 
    >>> g=my_range(0,3)
    >>> g
    <generator object my_range at 0x104105678>
    pper
    

    函数wrapper需要一个driver参数,而函数deco与wrapper的参数都有其特定的功能,不能用来接受其他类别的参数,可以在deco的外部再包一层函数auth,用来专门接受额外的参数,这样便保证了在auth函数内无论多少层都可以引用到

    def auth(driver):
        def deco(func):
            ……
        return deco
    

    此时我们就实现了一个有参装饰器,使用方式如下

    先调用auth_type(driver='file'),得到@deco,deco是一个闭包函数,
    包含了对外部作用域名字driver的引用,@deco的语法意义与无参装饰器一样
    @auth(driver='file') 
    def index():     
        pass
    @auth(driver='mysql') 
    def home():
        pass  
    

    可以使用help(函数名)来查看函数的文档注释,本质就是查看函数的doc属性,但对于被装饰之后的函数,查看文档注释

    @timer
    def home(name):
        '''
        home page function
        :param name: str
        :return: None
        '''
        time.sleep(5)
        print('Welcome to the home page',name)
    
    print(help(home))
    '''
    打印结果:
    
    Help on function wrapper in module __main__:
    
    wrapper(*args, **kwargs)
    
    None
    

    在被装饰之后home=wrapper,查看home.name也可以发现home的函数名确实是wrapper,想要保留原函数的文档和函数名属性,需要修正装饰器

    def timer(func):
        def wrapper(*args,**kwargs):
            start_time=time.time()
            res=func(*args,**kwargs)
            stop_time=time.time()
            print('run time is %s' %(stop_time-start_time))
            return res
        wrapper.__doc__=func.__doc__
        wrapper.__name__=func.__name__
        return wrapper
    

    按照上述方式来实现保留原函数属性过于麻烦,functools模块下提供一个装饰器wraps专门用来帮我们实现这件事,用法如下

    from functools import wraps
    
    def timer(func):
        @wraps(func)
        def wrapper(*args,**kwargs):
            start_time=time.time()
            res=func(*args,**kwargs)
            stop_time=time.time()
            print('run time is %s' %(stop_time-start_time))
            return res
        return wrapper
    
    标准版装饰器:
    def wrapper(func):
        def inner(*args,**kwargs):
            """执行被装饰函数前的操作"""
            func(*args,**kwargs)
            """执行被装饰函数后的操作"""
        return inner
    @wrapper
    def index():
        print("is index")
    index()
    
    语法糖要接受的变量就是语法糖下面的函数名、参数就是语法糖下面的函数名、调用的方式就是看调用的哪个语法糖
    def func(args):
        print("新加了一个功能")
        return args
    @func   #index = func(index)
    def index():
        print(2)
    index()
    结果:  新加了一个功能
    

    16、迭代器

    一 迭代器介绍

    迭代器即用来迭代取值的工具,而迭代是重复反馈过程的活动,其目的通常是为了逼近所需的目标或结果,每一次对过程的重复称为一次“迭代”,而每一次迭代得到的结果会作为下一次迭代的初始值,单纯的重复并不是迭代

    while True:
        msg = input('>>: ').strip()
        print(msg)
    

    下述while循环才是一个迭代过程,不仅满足重复,而且以每次重新赋值后的index值作为下一次循环中新的索引进行取值,反复迭代,最终可以取尽列表中的值

    goods=['mac','lenovo','acer','dell','sony']
    
    index=0
    while index < len(goods):
        print(goods[index])
        index+=1
    

    1.1 可迭代对象

    通过索引的方式进行迭代取值,实现简单,但仅适用于序列类型:字符串,列表,元组。对于没有索引的字典、集合等非序列类型,必须找到一种不依赖索引来进行迭代取值的方式,这就用到了迭代器。

    要想了解迭代器为何物,必须事先搞清楚一个很重要的概念:可迭代对象(Iterable)。从语法形式上讲,内置有__iter__方法的对象都是可迭代对象,字符串、列表、元组、字典、集合、打开的文件都是可迭代对象:

    {'name':'egon'}.__iter__
    {7,8,9}.__iter__
    ……
    

    1.2 迭代器对象

    调用obj.iter()方法返回的结果就是一个迭代器对象(Iterator)。迭代器对象是内置有iternext方法的对象,打开的文件本身就是一个迭代器对象,执行迭代器对象.iter()方法得到的仍然是迭代器本身,而执行迭代器.next()方法就会计算出迭代器中的下一个值。 迭代器是Python提供的一种统一的、不依赖于索引的迭代取值方式,只要存在多个“值”,无论序列类型还是非序列类型都可以按照迭代器的方式取值

    >>> s={1,2,3} # 可迭代对象s
    >>> i=iter(s)  # 本质就是在调用s.__iter__(),返回s的迭代器对象i,
    >>> next(i) # 本质就是在调用i.__next__()
    1
    >>> next(i)
    2
    >>> next(i)
    3
    >>> next(i)  #抛出StopIteration的异常,代表无值可取,迭代结束
    

    二 for循环原理

    有了迭代器后,我们便可以不依赖索引迭代取值了,使用while循环的实现方式如下

    goods=['mac','lenovo','acer','dell','sony']
    i=iter(goods) #每次都需要重新获取一个迭代器对象
    while True:
        try:
            print(next(i))
        except StopIteration: #捕捉异常终止循环
            break
    

    for循环又称为迭代循环,in后可以跟任意可迭代对象,上述while循环可以简写为

    goods=['mac','lenovo','acer','dell','sony']
    for item in goods:   
        print(item)
    

    for 循环在工作时,首先会调用可迭代对象goods内置的iter方法拿到一个迭代器对象,然后再调用该迭代器对象的next方法将取到的值赋给item,执行循环体完成一次循环,周而复始,直到捕捉StopIteration异常,结束迭代。

    三 迭代器的优缺点

    基于索引的迭代取值,所有迭代的状态都保存在了索引中,而基于迭代器实现迭代的方式不再需要索引,所有迭代的状态就保存在迭代器中,然而这种处理方式优点与缺点并存:

    3.1 优点:

    1、为序列和非序列类型提供了一种统一的迭代取值方式。

    2、惰性计算:迭代器对象表示的是一个数据流,可以只在需要时才去调用next来计算出一个值,就迭代器本身来说,同一时刻在内存中只有一个值,因而可以存放无限大的数据流,而对于其他容器类型,如列表,需要把所有的元素都存放于内存中,受内存大小的限制,可以存放的值的个数是有限的。

    3.2 缺点:

    1、除非取尽,否则无法获取迭代器的长度

    2、只能取下一个值,不能回到开始,更像是‘一次性的’,迭代器产生后的唯一目标就是重复执行next方法直到值取尽,否则就会停留在某个位置,等待下一次调用next;若是要再次迭代同个对象,你只能重新调用iter方法去创建一个新的迭代器对象,如果有两个或者多个循环使用同一个迭代器,必然只会有一个循环能取到值。

    17、生成器

    一 生成器与yield

    若函数体包含yield关键字,再调用函数,并不会执行函数体代码,得到的返回值即生成器对象

    >>> def my_range(start,stop,step=1):
    ...     print('start...')
    ...     while start < stop:
    ...         yield start
    ...         start+=step
    ...     print('end...')
    ... 
    >>> g=my_range(0,3)
    >>> g
    <generator object my_range at 0x104105678>
    

    一 包介绍

    随着模块数目的增多,把所有模块不加区分地放到一起也是极不合理的,于是Python为我们提供了一种把模块组织到一起的方法,即创建一个包。包就是一个含有__init__.py文件的文件夹,文件夹内可以组织子模块或子包,例如

    pool/                #顶级包
    ├── __init__.py     
    ├── futures          #子包
    │   ├── __init__.py
    │   ├── process.py
    │   └── thread.py
    └── versions.py      #子模块
    

    需要强调的是

    #1. 在python3中,即使包下没有__init__.py文件,import 包仍然不会报错,而在python2中,包下一定要有该文件,否则import 包报错
    
    #2. 创建包的目的不是为了运行,而是被导入使用,记住,包只是模块的一种形式而已,包的本质就是一种模
    

    接下来我们就以包pool为例来介绍包的使用,包内各文件内容如下

    # process.py
    class ProcessPoolExecutor:
        def __init__(self,max_workers):
            self.max_workers=max_workers
    
        def submit(self):
            print('ProcessPool submit')
    
    # thread.py
    class ThreadPoolExecutor:
        def __init__(self, max_workers):
            self.max_workers = max_workers
    
        def submit(self):
            print('ThreadPool submit')
    
    # versions.py
    def check():
        print('check versions’)
    
    # __init__.py文件内容均为空
    

    二 包的使用

    2.1 导入包与__init__.py

    包属于模块的一种,因而包以及包内的模块均是用来被导入使用的,而绝非被直接执行,首次导入包(如import pool)同样会做三件事:

    1、执行包下的__init__.py文件

    2、产生一个新的名称空间用于存放__init__.py执行过程中产生的名字

    3、在当前执行文件所在的名称空间中得到一个名字pool,该名字指向__init__.py的名称空间,例如http://pool.xxx和pool.yyy中的xxx和yyy都是来自于pool下的__init__.py,也就是说导入包时并不会导入包下所有的子模块与子包

    import pool
    
    pool.versions.check() #抛出异常AttributeError
    pool.futures.process.ProcessPoolExecutor(3) #抛出异常AttributeError
    

    pool.versions.check()要求pool下有名字versions,进而pool.versions下有名字check。pool.versions下已经有名字check了,所以问题出在pool下没有名字versions,这就需要在pool下的__init__.py中导入模块versions

    强调

    1.关于包相关的导入语句也分为importfrom ... import ...两种,但是无论哪种,无论在什么位置,在导入时都必须遵循一个原则:凡是在导入时带点的,点的左边都必须是一个包,否则非法。可以带有一连串的点,如import 顶级包.子包.子模块,但都必须遵循这个原则。但对于导入后,在使用时就没有这种限制了,点的左边可以是包,模块,函数,类(它们都可以用点的方式调用自己的属性)2、包A和包B下有同名模块也不会冲突,如A.a与B.a来自俩个命名空间
    
    3import导入文件时,产生名称空间中的名字来源于文件,import 包,产生的名称空间的名字同样来源于文件,即包下的__init__.py,导入包本质就是在导入该文件
    

    2.2 绝对导入与相对导入

    针对包内的模块之间互相导入,导入的方式有两种

    1、绝对导入:以顶级包为起始

    #pool下的__init__.py
    from pool import versions
    

    2、相对导入:.代表当前文件所在的目录,…代表当前目录的上一级目录,依此类推

    #pool下的__init__.py
    from . import versions
    

    同理,针对pool.futures.process.ProcessPoolExecutor(3),则需要

    #操作pool下的__init__.py,保证pool.futures
    from . import futures #或from pool import futures
    
    #操作futrues下的__init__.py,保证pool.futures.process
    from . import process #或from pool.futures import process
    

    在包内使用相对导入还可以跨目录导入模块,比如thread.py中想引用versions.py的名字check

    import也能使用绝对导入,导入过程中同样会依次执行包下的__init__.py,只是基于import导入
    

    例1:

    import pool.futures #拿到名字pool.futures指向futures下的__init__.py
    
    pool.futures.xxx #要求futures下的__init__.py中必须有名字xxx
    

    例2:

    import pool.futures.thread #拿到名字pool.futures.thread指向thread.py
    
    thread_pool=pool.futures.thread.ThreadPoolExecutor(3)
    thread_pool.submit()
    

    相对导入只能用from module import symbol的形式,import …versions语法是不对的,且symbol只能是一个明确的名字

    from pool import futures.process #语法错误
    from pool.futures import process #语法正确
    

    针对包内部模块之间的相互导入推荐使用相对导入,需要特别强调:

    1、相对导入只能在包内部使用,用相对导入不同目录下的模块是非法的

    2、无论是import还是from-import,但凡是在导入时带点的,点的左边必须是包,否则语法错误

    2.3 from 包 import *

    在使用包时同样支持from pool.futures import * ,毫无疑问代表的是futures下__init__.py中所有的名字,通用是用变量__all__来控制代表的意思

    #futures下的__init__.py
    __all__=['process','thread']
    

    最后说明一点,包内部的目录结构通常是包的开发者为了方便自己管理和维护代码而创建的,这种目录结构对包的使用者往往是无用的,此时通过操作__init__.py可以“隐藏”包内部的目录结构,降低使用难度,比如想要让使用者直接使用

    import pool
    
    pool.check()
    pool.ProcessPoolExecutor(3)
    pool.ThreadPoolExecutor(3)
    

    需要操作pool下的__init__.py

    from .versions import check
    from .futures.process import ProcessPoolExecutor
    from .futures.thread import ThreadPoolExecutor
    

    22、软件开发的目录规范

    软件开发目录规范

    为了提高程序的可读性与可维护性,我们应该为软件设计良好的目录结构,这与规范的编码风格同等重要。软件的目录规范并无硬性标准,只要清晰可读即可,假设你的软件名为foo,笔者推荐目录结构如下

    Foo/
    |-- core/
    |   |-- core.py
    |
    |-- api/
    |   |-- api.py
    |
    |-- db/
    |   |-- db_handle.py
    |
    |-- lib/
    |   |-- common.py
    |
    |-- conf/
    |   |-- settings.py
    |
    |-- run.py
    |-- setup.py
    |-- requirements.txt
    |-- README
    

    简要解释一下:

    • core/: 存放业务逻辑相关代码

    • api/: 存放接口文件,接口主要用于为业务逻辑提供数据操作。

    • db/: 存放操作数据库相关文件,主要用于与数据库交互

    • lib/: 存放程序中常用的自定义模块

    • conf/: 存放配置文件

    • run.py: 程序的启动文件,一般放在项目的根目录下,因为在运行时会默认将运行文件所在的文件夹作为sys.path的第一个路径,这样就省去了处理环境变量的步骤

    • setup.py: 安装、部署、打包的脚本。

    • requirements.txt: 存放软件依赖的外部Python包列表。

    • README: 项目说明文件。

    除此之外,有一些方案给出了更加多的内容,比如LICENSE.txt,ChangeLog.txt文件等,主要是在项目需要开源时才会用到,请读者自行查阅。

    关于README的内容,这个应该是每个项目都应该有的一个文件,目的是能简要描述该项目的信息,让读者快速了解这个项目。它需要说明以下几个事项:

    1、软件定位,软件的基本功能;
    
    2、运行代码的方法: 安装环境、启动命令等;
    
    3、简要的使用说明;
    
    4、代码目录结构说明,更详细点可以说明软件的基本原理;
    
    5、常见问题说明。
    

    关于setup.py和requirements.txt:

    一般来说,用setup.py来管理代码的打包、安装、部署问题。业界标准的写法是用Python流行的打包工具setuptools来管理这些事情,这种方式普遍应用于开源项目中。不过这里的核心思想不是用标准化的工具来解决这些问题,而是说,一个项目一定要有一个安装部署工具,能快速便捷的在一台新机器上将环境装好、代码部署好和将程序运行起来。

    requirements.txt文件的存在是为了方便开发者维护软件的依赖库。我们需要将开发过程中依赖库的信息添加进该文件中,避免在 setup.py安装依赖时漏掉软件包,同时也方便了使用者明确项目引用了哪些Python包。

    23、面向对象编程

    一 对象的概念

    ”面向对象“的核心是“对象”二字,而对象的精髓在于“整合“,

    所有的程序都是由”数据”与“功能“组成,因而编写程序的本质就是定义出一系列的数据,然后定义出一系列的功能来对数据进行操作。在学习”对象“之前,程序中的数据与功能是分离开的,如下

    # 数据:name、age、sex
    name='lili'
    age=18
    sex='female'
    
    # 功能:tell_info
    def tell_info(name,age,sex): 
        print('<%s:%s:%s>' %(name,age,sex))
    
    # 此时若想执行查看个人信息的功能,需要同时拿来两样东西,一类是功能tell_info,另外一类则是多个数据name、age、sex,然后才能执行,非常麻烦
    tell_info(name,age,sex)
    

    二 类与对象

    类即类别/种类,是面向对象分析和设计的基石,如果多个对象有相似的数据与功能,那么该多个对象就属于同一种类。有了类的好处是:我们可以把同一类对象相同的数据与功能存放到类里,而无需每个对象都重复存一份,这样每个对象里只需存自己独有的数据即可,极大地节省了空间。所以,如果说对象是用来存放数据与功能的容器,那么类则是用来存放多个对象相同的数据与功能的容器。

    综上所述,虽然我们是先介绍对象后介绍类,但是需要强调的是:在程序中,必须要事先定义类,然后再调用类产生对象(调用类拿到的返回值就是对象)。产生对象的类与对象之间存在关联,这种关联指的是:对象可以访问到类中共有的数据与功能,所以类中的内容仍然是属于对象的,类只不过是一种节省空间、减少代码冗余的机制,面向对象编程最终的核心仍然是去使用对象。

    三 面向对象编程

    3.1 类的定义与实例化

    类体最常见的是变量的定义和函数的定义,但其实类体可以包含任意Python代码,类体的代码在类定义阶段就会执行,因而会产生新的名称空间用来存放类中定义的名字,可以打印Student.__dict__来查看类这个容器内盛放的东西

    调用类的过程称为将类实例化,拿到的返回值就是程序中的对象,或称为一个实例

    如此stu1、stu2、stu3全都一样了(只有类中共有的内容,而没有各自独有的数据),想在实例化的过程中就为三位学生定制各自独有的数据:姓名,性别,年龄,需要我们在类内部新增一个__init__方法,如下

    class Student:
        school='清华大学'
    
        #该方法会在对象产生之后自动执行,专门为对象进行初始化操作,可以有任意代码,但一定不能返回非None的值
        def __init__(self,name,sex,age):
            self.name=name
            self.sex=sex
            self.age=age
    
        def choose(self): 
            print('%s is choosing a course' %self.name)
    

    然后我们重新实例出三位学生

    >>> stu1=Student('李建刚','男',28)
    >>> stu2=Student('王大力','女',18)
    >>> stu3=Student('牛嗷嗷','男',38)
    

    单拿stu1的产生过程来分析,调用类会先产生一个空对象stu1,然后将stu1连同调用类时括号内的参数一起传给Student.init(stu1,’李建刚’,’男’,28)

    def __init__(self, name, sex, age):
        self.name = name  # stu1.name = '李建刚'
        self.sex = sex    # stu1.sex = '男'
        self.age = age    # stu1.age = 28
    

    会产生对象的名称空间,同样可以用__dict__查看

    >>> stu1.__dict__
    {'name': '李建刚', 'sex': '男', 'age': 28}
    

    3.2 属性访问

    3.2.1 类属性与对象属性

    在类中定义的名字,都是类的属性,细说的话,类有两种属性:数据属性和函数属性,可以通过__dict__访问属性的值,比如Student.dict[‘school’],但Python提供了专门的属性访问语法

    3.2.2 属性查找顺序与绑定方法

    对象的名称空间里只存放着对象独有的属性,而对象们相似的属性是存放于类中的。对象在访问属性时,会优先从对象本身的__dict__中查找,未找到,则去类的__dict__中查找

    1、类中定义的变量是类的数据属性,是共享给所有对象用的,指向相同的内存地址

    # id都一样
    print(id(Student.school)) # 4301108704
    
    print(id(stu1.school)) # 4301108704
    print(id(stu2.school)) # 4301108704
    print(id(stu3.school)) # 4301108704
    

    2、类中定义的函数是类的函数属性,类可以使用,但必须遵循函数的参数规则,有几个参数需要传几个参数

    Student.choose(stu1) # 李建刚 is choosing a course
    Student.choose(stu2) # 王大力 is choosing a course
    Student.choose(stu3) # 牛嗷嗷 is choosing a course
    

    但其实类中定义的函数主要是给对象使用的,而且是绑定给对象的,虽然所有对象指向的都是相同的功能,但是绑定到不同的对象就是不同的绑定方法,内存地址各不相同

    print(id(Student.choose)) # 4335426280
    
    print(id(stu1.choose)) # 4300433608
    print(id(stu2.choose)) # 4300433608
    print(id(stu3.choose)) # 4300433608
    

    绑定到对象的方法特殊之处在于,绑定给谁就应该由谁来调用,谁来调用,就会将’谁’本身当做第一个参数自动传入(方法__init__也是一样的道理)

    stu1.choose()  # 等同于Student.choose(stu1)
    stu2.choose()  # 等同于Student.choose(stu2)
    stu3.choose()  # 等同于Student.choose(stu3)
    

    绑定到不同对象的choose技能,虽然都是选课,但李建刚选的课,不会选给王大力,这正是”绑定“二字的精髓所在。

    #注意:绑定到对象方法的这种自动传值的特征,决定了在类中定义的函数都要默认写一个参数self,self可以是任意名字,但命名为self是约定俗成的。
    

    Python中一切皆为对象,且Python3中类与类型是一个概念,因而绑定方法我们早就接触过

    #类型list就是类
    >>> list
    <class 'list'>
    
    #实例化的到3个对象l1,l2,l3
    >>> l1=list([1,2,3])
    >>> l2=list(['a','b','c'])
    >>> l3=list(['x','y'])
    
    #三个对象都有绑定方法append,是相同的功能,但内存地址不同
    >>> l1.append
    <built-in method append of list object at 0x10b482b48>
    >>> l2.append
    <built-in method append of list object at 0x10b482b88>
    >>> l3.append
    <built-in method append of list object at 0x10b482bc8>
    
    #操作绑定方法l1.append(4),就是在往l1添加4,绝对不会将4添加到l2或l3
    >>> l1.append(4) #等同于list.append(l1,4)
    >>> l1
    [1,2,3,4]
    >>> l2
    ['a','b','c']
    >>> l3
    ['x','y']
    

    3.3.3 小结

    在上述介绍类与对象的使用过程中,我们更多的是站在底层原理的角度去介绍类与对象之间的关联关系,如果只是站在使用的角度,我们无需考虑语法“对象.属性"中”属性“到底源自于哪里,只需要知道是通过对象获取到的就可以了,所以说,对象是一个高度整合的产物,有了对象,我们只需要使用”对象.xxx“的语法就可以得到跟这个对象相关的所有数据与功能,十分方便且解耦合程度极高。

    24、封装

    一 引入

    面向对象编程有三大特性:封装、继承、多态,其中最重要的一个特性就是封装。封装指的就是把数据与功能都整合到一起,听起来是不是很熟悉,没错,我们之前所说的”整合“二字其实就是封装的通俗说法。除此之外,针对封装到对象或者类中的属性,我们还可以严格控制对它们的访问,分两步实现:隐藏与开放接口

    二 隐藏属性

    Python的Class机制采用双下划线开头的方式将属性隐藏起来(设置成私有的),但其实这仅仅只是一种变形操作,类中所有双下滑线开头的属性都会在类定义阶段、检测语法时自动变成“_类名__属性名”的形式:

    class Foo:
        __N=0 # 变形为_Foo__N
    
        def __init__(self): # 定义函数时,会检测函数语法,所以__开头的属性也会变形
            self.__x=10 # 变形为self._Foo__x
    
        def __f1(self): # 变形为_Foo__f1
            print('__f1 run')
    
        def f2(self):  # 定义函数时,会检测函数语法,所以__开头的属性也会变形
            self.__f1() #变形为self._Foo__f1()
    
    print(Foo.__N) # 报错AttributeError:类Foo没有属性__N
    
    obj = Foo()
    print(obbj.__x) # 报错AttributeError:对象obj没有属性__x
    

    这种变形需要注意的问题是:

    1、在类外部无法直接访问双下滑线开头的属性,但知道了类名和属性名就可以拼出名字:_类名__属性,然后就可以访问了,如Foo._A__N,所以说这种操作并没有严格意义上地限制外部访问,仅仅只是一种语法意义上的变形。

    >>> Foo.__dict__
    mappingproxy({..., '_Foo__N': 0, ...})
    
    >>> obj.__dict__
    {'_Foo__x': 10}
    
    >>> Foo._Foo__N
    0
    >>> obj._Foo__x
    10
    >>> obj._Foo__N
    0
    

    2、在类内部是可以直接访问双下滑线开头的属性的,比如self.__f1(),因为在类定义阶段类内部双下滑线开头的属性统一发生了变形。

    >>> obj.f2()
    __f1 run
    

    3、变形操作只在类定义阶段发生一次,在类定义之后的赋值操作,不会变形。

    >>> Foo.__M=100
    >>> Foo.__dict__
    mappingproxy({..., '__M': 100,...})
    >>> Foo.__M
    100
    
    >>> obj.__y=20
    >>> obj.__dict__
    {'__y': 20, '_Foo__x': 10}
    >>> obj.__y
    20
    

    三 开放接口

    定义属性就是为了使用,所以隐藏并不是目的

    3.1 隐藏数据属性

    将数据隐藏起来就限制了类外部对数据的直接操作,然后类内应该提供相应的接口来允许类外部间接地操作数据,接口之上可以附加额外的逻辑来对数据的操作进行严格地控制

    >>> class Teacher:
    ...     def __init__(self,name,age): #将名字和年纪都隐藏起来
    ...         self.__name=name
    ...         self.__age=age
    ...     def tell_info(self): #对外提供访问老师信息的接口
    ...         print('姓名:%s,年龄:%s' %(self.__name,self.__age))
    ...     def set_info(self,name,age): #对外提供设置老师信息的接口,并附加类型检查的逻辑
    ...         if not isinstance(name,str):
    ...             raise TypeError('姓名必须是字符串类型')
    ...         if not isinstance(age,int):
    ...             raise TypeError('年龄必须是整型')
    ...         self.__name=name
    ...         self.__age=age
    ... 
    >>>
    >>> t=Teacher('lili',18)
    >>> t.set_info(‘LiLi','19') # 年龄不为整型,抛出异常
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      File "<stdin>", line 11, in set_info
    TypeError: 年龄必须是整型
    >>> t.set_info('LiLi',19) # 名字为字符串类型,年龄为整形,可以正常设置
    >>> t.tell_info() # 查看老师的信息
    姓名:LiLi,年龄:19
    

    3.2 隐藏函数属性

    目的的是为了隔离复杂度,例如ATM程序的取款功能,该功能有很多其他功能组成,比如插卡、身份认证、输入金额、打印小票、取钱等,而对使用者来说,只需要开发取款这个功能接口即可,其余功能我们都可以隐藏起来

    >>> class ATM:
    ...     def __card(self): #插卡
    ...         print('插卡')
    ...     def __auth(self): #身份认证
    ...         print('用户认证')
    ...     def __input(self): #输入金额
    ...         print('输入取款金额')
    ...     def __print_bill(self): #打印小票
    ...         print('打印账单')
    ...     def __take_money(self): #取钱
    ...         print('取款')
    ...     def withdraw(self): #取款功能
    ...         self.__card()
    ...         self.__auth()
    ...         self.__input()
    ...         self.__print_bill()
    ...         self.__take_money()
    ...
    >>> obj=ATM()
    >>> obj.withdraw()
    

    总结隐藏属性与开放接口,本质就是为了明确地区分内外,类内部可以修改封装内的东西而不影响外部调用者的代码;而类外部只需拿到一个接口,只要接口名、参数不变,则无论设计者如何改变内部实现代码,使用者均无需改变代码。这就提供一个良好的合作基础,只要接口这个基础约定不变,则代码的修改不足为虑。

    四 property

    >>> class People:
    ...     def __init__(self,name,weight,height):
    ...         self.name=name
    ...         self.weight=weight
    ...         self.height=height
    ...     @property
    ...     def bmi(self):
    ...         return self.weight / (self.height**2)
    ...
    >>> obj=People('lili',75,1.85)
    >>> obj.bmi #触发方法bmi的执行,将obj自动传给self,执行后返回值作为本次引用的结果
    21.913805697589478
    

    使用property有效地保证了属性访问的一致性。另外property还提供设置和删除属性的功能,如下

    >>> class Foo:
    ...     def __init__(self,val):
    ...         self.__NAME=val #将属性隐藏起来
    ...     @property
    ...     def name(self):
    ...         return self.__NAME
    ...     @name.setter
    ...     def name(self,value):
    ...         if not isinstance(value,str):  #在设定值之前进行类型检查
    ...             raise TypeError('%s must be str' %value)
    ...         self.__NAME=value #通过类型检查后,将值value存放到真实的位置self.__NAME
    ...     @name.deleter
    ...     def name(self):
    ...         raise PermissionError('Can not delete')
    ...
    >>> f=Foo('lili')
    >>> f.name
    lili
    >>> f.name='LiLi' #触发name.setter装饰器对应的函数name(f,’Egon')
    >>> f.name=123 #触发name.setter对应的的函数name(f,123),抛出异常TypeError
    >>> del f.name #触发name.deleter对应的函数name(f),抛出异常PermissionError
    

    25、继承与派生

    一 继承介绍

    继承是一种创建新类的方式,在Python中,新建的类可以继承一个或多个父类,新建的类可称为子类或派生类,父类又可称为基类或超类

    class ParentClass1: #定义父类
        pass
    
    class ParentClass2: #定义父类
        pass
    
    class SubClass1(ParentClass1): #单继承
        pass
    
    class SubClass2(ParentClass1,ParentClass2): #多继承
        pass
    

    通过类的内置属性__bases__可以查看类继承的所有父类

    >>> SubClass2.__bases__
    (<class '__main__.ParentClass1'>, <class '__main__.ParentClass2'>)
    

    在Python2中有经典类与新式类之分,没有显式地继承object类的类,以及该类的子类,都是经典类,显式地继承object的类,以及该类的子类,都是新式类。而在Python3中,即使没有显式地继承object,也会默认继承该类,如下

    >>> ParentClass1.__bases__
    (<classobject'>,)
    >>> ParentClass2.__bases__
    (<class 'object'>,)
    

    因而在Python3中统一都是新式类,关于经典类与新式类的区别,我们稍后讨论

    提示:object类提供了一些常用内置方法的实现,如用来在打印对象时返回字符串的内置方法__str__
    

    二 继承与抽象

    要找出类与类之间的继承关系,需要先抽象,再继承。抽象即总结相似之处,总结对象之间的相似之处得到类,总结类与类之间的相似之处就可以得到父类,如下图所示

    基于抽象的结果,我们就找到了继承关系

    基于上图我们可以看出类与类之间的继承指的是什么’是’什么的关系(比如人类,猪类,猴类都是动物类)。子类可以继承/遗传父类所有的属性,因而继承可以用来解决类与类之间的代码重用性问题。比如我们按照定义Student类的方式再定义一个Teacher类

    class Teacher:
        school='清华大学'
    
        def __init__(self,name,sex,age):
            self.name=name
            self.sex=sex
            self.age=age
    
        def teach(self):
            print('%s is teaching' %self.name)
    

    类Teacher与Student之间存在重复的代码,老师与学生都是人类,所以我们可以得出如下继承关系,实现代码重用

    class People:
        school='清华大学'
    
        def __init__(self,name,sex,age):
            self.name=name
            self.sex=sex
            self.age=age
    
    class Student(People):
        def choose(self):
            print('%s is choosing a course' %self.name)
    
    class Teacher(People):
        def teach(self):
            print('%s is teaching' %self.name)
    

    Teacher类内并没有定义__init__方法,但是会从父类中找到__init__,因而仍然可以正常实例化,如下

    >>> teacher1=Teacher('lili','male',18)
    >>> teacher1.school,teacher1.name,teacher1.sex,teacher1.age
    ('清华大学', 'lili', 'male', 18)
    

    三 属性查找

    有了继承关系,对象在查找属性时,先从对象自己的__dict__中找,如果没有则去子类中找,然后再去父类中找……

    >>> class Foo:
    ...     def f1(self):
    ...         print('Foo.f1')
    ...     def f2(self):
    ...         print('Foo.f2')
    ...         self.f1()
    ... 
    >>> class Bar(Foo):
    ...     def f1(self):
    ...         print('Foo.f1')
    ... 
    >>> b=Bar()
    >>> b.f2()
    Foo.f2
    Foo.f1
    

    b.f2()会在父类Foo中找到f2,先打印Foo.f2,然后执行到self.f1(),即b.f1(),仍会按照:对象本身->类Bar->父类Foo的顺序依次找下去,在类Bar中找到f1,因而打印结果为Foo.f1

    父类如果不想让子类覆盖自己的方法,可以采用双下划线开头的方式将方法设置为私有的

    >>> class Foo:
    ...     def __f1(self): # 变形为_Foo__fa
    ...         print('Foo.f1') 
    ...     def f2(self):
    ...         print('Foo.f2')
    ...         self.__f1() # 变形为self._Foo__fa,因而只会调用自己所在的类中的方法
    ... 
    >>> class Bar(Foo):
    ...     def __f1(self): # 变形为_Bar__f1
    ...         print('Foo.f1')
    ... 
    >>> 
    >>> b=Bar()
    >>> b.f2() #在父类中找到f2方法,进而调用b._Foo__f1()方法,同样是在父类中找到该方法
    Foo.f2
    Foo.f1
    

    四 继承的实现原理

    4.1 菱形问题

    大多数面向对象语言都不支持多继承,而在Python中,一个子类是可以同时继承多个父类的,这固然可以带来一个子类可以对多个不同父类加以重用的好处,但也有可能引发著名的 Diamond problem菱形问题(或称钻石问题,有时候也被称为“死亡钻石”),菱形其实就是对下面这种继承结构的形象比喻

    img

    这种继承结构下导致的问题称之为菱形问题:如果A中有一个方法,B和/或C都重写了该方法,而D没有重写它,那么D继承的是哪个版本的方法:B的还是C的?如下所示

    class A(object):
        def test(self):
            print('from A')
    
    
    class B(A):
        def test(self):
            print('from B')
    
    
    class C(A):
        def test(self):
            print('from C')
    
    
    class D(B,C):
        pass
    
    
    obj = D()
    obj.test() # 结果为:from B
    

    要想搞明白obj.test()是如何找到方法test的,需要了解python的继承实现原理

    4.2 继承原理

    python到底是如何实现继承的呢? 对于你定义的每一个类,Python都会计算出一个方法解析顺序(MRO)列表,该MRO列表就是一个简单的所有基类的线性顺序列表,如下

    >>> D.mro() # 新式类内置了mro方法可以查看线性列表的内容,经典类没有该内置该方法
    [<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>]
    

    python会在MRO列表上从左到右开始查找基类,直到找到第一个匹配这个属性的类为止。 而这个MRO列表的构造是通过一个C3线性化算法来实现的。我们不去深究这个算法的数学原理,它实际上就是合并所有父类的MRO列表并遵循如下三条准则:

    1.子类会先于父类被检查
    2.多个父类会根据它们在列表中的顺序被检查
    3.如果对下一个类存在两个合法的选择,选择第一个父类
    

    所以obj.test()的查找顺序是,先从对象obj本身的属性里找方法test,没有找到,则参照属性查找的发起者(即obj)所处类D的MRO列表来依次检索,首先在类D中未找到,然后再B中找到方法test

    ps:

    1.由对象发起的属性查找,会从对象自身的属性里检索,没有则会按照对象的类.mro()规定的顺序依次找下去,
    2.由类发起的属性查找,会按照当前类.mro()规定的顺序依次找下去,
    

    4.3 深度优先和广度优先

    参照下述代码,多继承结构为非菱形结构,此时,会按照先找B这一条分支,然后再找C这一条分支,最后找D这一条分支的顺序直到找到我们想要的属性

    4.4 Pyton Mixins机制

    一个子类可以同时继承多个父类,这样的设计常被人诟病,一来它有可能导致可恶的菱形问题,二来在人的世界观里继承应该是个”is-a”关系。 比如轿车类之所以可以继承交通工具类,是因为基于人的世界观,我们可以说:轿车是一个(“is-a”)交通工具,而在人的世界观里,一个物品不可能是多种不同的东西,因此多重继承在人的世界观里是说不通的,它仅仅只是代码层面的逻辑。不过有没有这种情况,一个类的确是需要继承多个类呢?

    答案是有,我们还是拿交通工具来举例子:

    民航飞机、直升飞机、轿车都是一个(is-a)交通工具,前两者都有一个功能是飞行fly,但是轿车没有,所以如下所示我们把飞行功能放到交通工具这个父类中是不合理的

    class Vehicle:  # 交通工具
        def fly(self):
            '''
            飞行功能相应的代码        
            '''
            print("I am flying")
    
    
    class CivilAircraft(Vehicle):  # 民航飞机
        pass
    
    
    class Helicopter(Vehicle):  # 直升飞机
        pass
    
    
    class Car(Vehicle):  # 汽车并不会飞,但按照上述继承关系,汽车也能飞了
        pass
    

    但是如果民航飞机和直升机都各自写自己的飞行fly方法,又违背了代码尽可能重用的原则(如果以后飞行工具越来越多,那会重复代码将会越来越多)。

    怎么办???为了尽可能地重用代码,那就只好在定义出一个飞行器的类,然后让民航飞机和直升飞机同时继承交通工具以及飞行器两个父类,这样就出现了多重继承。这时又违背了继承必须是”is-a”关系。这个难题该怎么解决?

    不同的语言给出了不同的方法,让我们先来了解Java的处理方法。Java提供了接口interface功能,来实现多重继承:

    // 抽象基类:交通工具类
    public abstract class Vehicle {
    }
    
    // 接口:飞行器
    public interface Flyable {
        public void fly();
    }
    
    // 类:实现了飞行器接口的类,在该类中实现具体的fly方法,这样下面民航飞机与直升飞机在实现fly时直接重用即可
    public class FlyableImpl implements Flyable {
        public void fly() {
            System.out.println("I am flying");
        }
    }
    
    
    
    // 民航飞机,继承自交通工具类,并实现了飞行器接口
    public class CivilAircraft extends Vehicle implements Flyable {
        private Flyable flyable;
    
        public CivilAircraft() {
            flyable = new FlyableImpl();
        }
    
        public void fly() {
            flyable.fly();
        }
    }
    
    // 直升飞机,继承自交通工具类,并实现了飞行器接口
    public class Helicopter extends Vehicle implements Flyable {
        private Flyable flyable;
    
        public Helicopter() {
            flyable = new FlyableImpl();
        }
    
        public void fly() {
            flyable.fly();
        }
    }
    
    // 汽车,继承自交通工具类,
    public class Car extends Vehicle {
    }
    

    现在我们的飞机同时具有了交通工具及飞行器两种属性,而且我们不需要重写飞行器中的飞行方法,同时我们没有破坏单一继承的原则。飞机就是一种交通工具,可飞行的能力是飞机的属性,通过继承接口来获取。

    回到主题,Python语言可没有接口功能,但Python提供了Mixins机制,简单来说Mixins机制指的是子类混合(mixin)不同类的功能,而这些类采用统一的命名规范(例如Mixin后缀),以此标识这些类只是用来混合功能的,并不是用来标识子类的从属"is-a"关系的,所以Mixins机制本质仍是多继承,但同样遵守”is-a”关系,如下

    class Vehicle:  # 交通工具
        pass
    
    
    class FlyableMixin:
        def fly(self):
            '''
            飞行功能相应的代码        
            '''
            print("I am flying")
    
    
    class CivilAircraft(FlyableMixin, Vehicle):  # 民航飞机
        pass
    
    
    class Helicopter(FlyableMixin, Vehicle):  # 直升飞机
        pass
    
    
    class Car(Vehicle):  # 汽车
        pass
    
    # ps: 采用某种规范(如命名规范)来解决具体的问题是python惯用的套路
    

    可以看到,上面的CivilAircraft、Helicopter类实现了多继承,不过它继承的第一个类我们起名为FlyableMixin,而不是Flyable,这个并不影响功能,但是会告诉后来读代码的人,这个类是一个Mixin类,表示混入(mix-in),这种命名方式就是用来明确地告诉别人(python语言惯用的手法),这个类是作为功能添加到子类中,而不是作为父类,它的作用同Java中的接口。所以从含义上理解,CivilAircraft、Helicopter类都只是一个Vehicle,而不是一个飞行器。

    使用Mixin类实现多重继承要非常小心

    • 首先它必须表示某一种功能,而不是某个物品,python 对于mixin类的命名方式一般以 Mixin, able, ible 为后缀
    • 其次它必须责任单一,如果有多个功能,那就写多个Mixin类,一个类可以继承多个Mixin,为了保证遵循继承的“is-a”原则,只能继承一个标识其归属含义的父类
    • 然后,它不依赖于子类的实现
    • 最后,子类即便没有继承这个Mixin类,也照样可以工作,就是缺少了某个功能。(比如飞机照样可以载客,就是不能飞了)

    Mixins是从多个类中重用代码的好方法,但是需要付出相应的代价,我们定义的Minx类越多,子类的代码可读性就会越差,并且更恶心的是,在继承的层级变多时,代码阅读者在定位某一个方法到底在何处调用时会晕头转向,如下

    class Displayer:
        def display(self, message):
            print(message)
    
    
    class LoggerMixin:
        def log(self, message, filename='logfile.txt'):
            with open(filename, 'a') as fh:
                fh.write(message)
    
        def display(self, message):
            super().display(message) # super的用法请参考下一小节
            self.log(message)
    
    
    class MySubClass(LoggerMixin, Displayer):
        def log(self, message):
            super().log(message, filename='subclasslog.txt') 
    
    
    obj = MySubClass()
    obj.display("This string will be shown and logged in subclasslog.txt")
    
    
    # 属性查找的发起者是obj,所以会参照类MySubClass的MRO来检索属性
    #[<class '__main__.MySubClass'>, <class '__main__.LoggerMixin'>, <class '__main__.Displayer'>, <class 'object'>]
    
    # 1、首先会去对象obj的类MySubClass找方法display,没有则去类LoggerMixin中找,找到开始执行代码
    # 2、执行LoggerMixin的第一行代码:执行super().display(message),参照MySubClass.mro(),super会去下一个类即类Displayer中找,找到display,开始执行代码,打印消息"This string will be shown and logged in subclasslog.txt"
    # 3、执行LoggerMixin的第二行代码:self.log(message),self是对象obj,即obj.log(message),属性查找的发起者为obj,所以会按照其类MySubClass.mro(),即MySubClass->LoggerMixin->Displayer->object的顺序查找,在MySubClass中找到方法log,开始执行super().log(message, filename='subclasslog.txt'),super会按照MySubClass.mro()查找下一个类,在类LoggerMixin中找到log方法开始执行,最终将日志写入文件subclasslog.txt
    

    ps:课外了解小知识

    Java只允许接口的多重继承。接口本质上是抽象基类,具有所有抽象方法,没有数据成员。
    与java一样,python也有抽象类的概念但是同样需要借助模块实现,抽象类是一个特殊的类,它的特殊之处在于只能被继承,不能被实例化,继承的子类必须实现抽象基类规定的方法,这样便可保证始终只有一个特定方法或属性的实现,并且不会产生歧义,因而也可以起到避免菱形问题的作用
    
    java的interface:https://www.cnblogs.com/linhaifeng/articles/7340153.html#_label6
    python的抽象基类:https://www.cnblogs.com/linhaifeng/articles/7340153.html#_label7
    

    五 派生与方法重用

    子类可以派生出自己新的属性,在进行属性查找时,子类中的属性名会优先于父类被查找,例如每个老师还有职称这一属性,我们就需要在Teacher类中定义该类自己的__init__覆盖父类的

    >>> class People:
    ...     school='清华大学'
    ...     
    ...     def __init__(self,name,sex,age):
    ...         self.name=name
    ...         self.sex=sex
    ...         self.age=age
    ... 
    >>> class Teacher(People):
    ...     def __init__(self,name,sex,age,title): # 派生
    ...         self.name=name
    ...         self.sex=sex
    ...         self.age=age
    ...         self.title=title
    ...     def teach(self):
    ...         print('%s is teaching' %self.name)
    ... 
    >>> obj=Teacher('lili','female',28,'高级讲师') #只会找自己类中的__init__,并不会自动调用父类的
    >>> obj.name,obj.sex,obj.age,obj.title
    ('lili', 'female', 28, '高级讲师')
    

    很明显子类Teacher中__init__内的前三行又是在写重复代码,若想在子类派生出的方法内重用父类的功能,有两种实现方式

    方法一:“指名道姓”地调用某一个类的函数

    >>> class Teacher(People):
    ...     def __init__(self,name,sex,age,title):
    ...         People.__init__(self,name,age,sex) #调用的是函数,因而需要传入self
    ...         self.title=title
    ...     def teach(self):
    ...         print('%s is teaching' %self.name)
    ...
    

    方法二:super()

    调用super()会得到一个特殊的对象,该对象专门用来引用父类的属性,且严格按照MRO规定的顺序向后查找

    >>> class Teacher(People):
    ...     def __init__(self,name,sex,age,title):
    ...         super().__init__(name,age,sex) #调用的是绑定方法,自动传入self
    ...         self.title=title
    ...     def teach(self):
    ...         print('%s is teaching' %self.name)
    ...
    

    提示:在Python2中super的使用需要完整地写成super(自己的类名,self) ,而在python3中可以简写为super()。

    这两种方式的区别是:方式一是跟继承没有关系的,而方式二的super()是依赖于继承的,并且即使没有直接继承关系,super()仍然会按照MRO继续往后查找

    >>> #A没有继承B
    ... class A:
    ...     def test(self):
    ...         super().test()
    ... 
    >>> class B:
    ...     def test(self):
    ...         print('from B')
    ... 
    >>> class C(A,B):
    ...     pass
    ... 
    >>> C.mro() # 在代码层面A并不是B的子类,但从MRO列表来看,属性查找时,就是按照顺序C->A->B->object,B就相当于A的“父类”
    [<class '__main__.C'>, <class '__main__.A'>, <class '__main__.B'>,<classobject'>]
    >>> obj=C()
    >>> obj.test() # 属性查找的发起者是类C的对象obj,所以中途发生的属性查找都是参照C.mro()
    from B
    

    obj.test()首先找到A下的test方法,执行super().test()会基于MRO列表(以C.mro()为准)当前所处的位置继续往后查找(),然后在B中找到了test方法并执行。

    关于在子类中重用父类功能的这两种方式,使用任何一种都可以,但是在最新的代码中还是推荐使用super()

    六 组合

    在一个类中以另外一个类的对象作为数据属性,称为类的组合。组合与继承都是用来解决代码的重用性问题。不同的是:继承是一种“是”的关系,比如老师是人、学生是人,当类之间有很多相同的之处,应该使用继承;而组合则是一种“有”的关系,比如老师有生日,老师有多门课程,当类之间有显著不同,并且较小的类是较大的类所需要的组件时,应该使用组合,如下示例

    class Course:
        def __init__(self,name,period,price):
            self.name=name
            self.period=period
            self.price=price
        def tell_info(self):
            print('<%s %s %s>' %(self.name,self.period,self.price))
    
    class Date:
        def __init__(self,year,mon,day):
            self.year=year
            self.mon=mon
            self.day=day
        def tell_birth(self):
           print('<%s-%s-%s>' %(self.year,self.mon,self.day))
    
    class People:
        school='清华大学'
        def __init__(self,name,sex,age):
            self.name=name
            self.sex=sex
            self.age=age
    
    #Teacher类基于继承来重用People的代码,基于组合来重用Date类和Course类的代码
    class Teacher(People): #老师是人
        def __init__(self,name,sex,age,title,year,mon,day):
            super().__init__(name,age,sex)
            self.birth=Date(year,mon,day) #老师有生日
            self.courses=[] #老师有课程,可以在实例化后,往该列表中添加Course类的对象
        def teach(self):
            print('%s is teaching' %self.name)
    
    
    python=Course('python','3mons',3000.0)
    linux=Course('linux','5mons',5000.0)
    teacher1=Teacher('lili','female',28,'博士生导师',1990,3,23)
    
    # teacher1有两门课程
    teacher1.courses.append(python)
    teacher1.courses.append(linux)
    
    # 重用Date类的功能
    teacher1.birth.tell_birth()
    
    # 重用Course类的功能
    for obj in teacher1.courses: 
        obj.tell_info()
    

    此时对象teacher1集对象独有的属性、Teacher类中的内容、Course类中的内容于一身(都可以访问到),是一个高度整合的产物

    26、多态性与鸭子类型

    多态与多态性

    多态指的是一类事物有多种形态,比如动物有多种形态:猫、狗、猪

    class Animal: #同一类事物:动物
        def talk(self):
            pass
    class Cat(Animal): #动物的形态之一:猫
        def talk(self):
            print('喵喵喵')
    class Dog(Animal): #动物的形态之二:狗
        def talk(self):
            print('汪汪汪')
    class Pig(Animal): #动物的形态之三:猪
        def talk(self):
            print('哼哼哼')
    
    #实例化得到三个对象
    >>> cat=Cat()
    >>> dog=Dog()
    >>> pig=Pig()
    

    多态性指的是可以在不用考虑对象具体类型的情况下而直接使用对象,这就需要在设计时,把对象的使用方法统一成一种:例如cat、dog、pig都是动物,但凡是动物肯定有talk方法,于是我们可以不用考虑它们三者的具体是什么类型的动物,而直接使用

    >>> cat.talk()
    喵喵喵
    >>> dog.talk()
    汪汪汪
    >>> pig.talk()
    哼哼哼
    

    更进一步,我们可以定义一个统一的接口来使用

    >>> def Talk(animal):
    ...     animal.talk()
    ... 
    >>> Talk(cat)
    喵喵喵
    >>> Talk(dog)
    汪汪汪
    >>> Talk(pig)
    哼哼哼
    

    Python中一切皆对象,本身就支持多态性

    # 我们可以在不考虑三者类型的情况下直接使用统计三个对象的长度
    s.__len__()
    l.__len__()
    t.__len__()
    
    # Python内置了一个统一的接口
    len(s)
    len(l)
    len(t)
    

    多态性的好处在于增强了程序的灵活性和可扩展性,比如通过继承Animal类创建了一个新的类,实例化得到的对象obj,可以使用相同的方式使用obj.talk()

    >>> class Wolf(Animal): #动物的另外一种形态:狼
    ...     def talk(self):
    ...         print('嗷...')
    ... 
    >>> wolf=Wolf() # 实例出一头狼
    >>> wolf.talk() # 使用者根本无需关心wolf是什么类型而调用talk...
    

    综上我们得知,多态性的本质在于不同的类中定义有相同的方法名,这样我们就可以不考虑类而统一用一种方式去使用对象,可以通过在父类引入抽象类的概念来硬性限制子类必须有某些方法名

    import abc
    
    # 指定metaclass属性将类设置为抽象类,抽象类本身只是用来约束子类的,不能被实例化
    class Animal(metaclass=abc.ABCMeta):
        @abc.abstractmethod # 该装饰器限制子类必须定义有一个名为talk的方法
        def talk(self): # 抽象方法中无需实现具体的功能
            pass
    
    class Cat(Animal): # 但凡继承Animal的子类都必须遵循Animal规定的标准
        def talk(self):
            pass
    
    cat=Cat() # 若子类中没有一个名为talk的方法则会抛出异常TypeError,无法实例化
    

    但其实我们完全可以不依赖于继承,只需要制造出外观和行为相同对象,同样可以实现不考虑对象类型而使用对象,这正是Python崇尚的“鸭子类型”(duck typing):“如果看起来像、叫声像而且走起路来像鸭子,那么它就是鸭子”。比起继承的方式,鸭子类型在某种程度上实现了程序的松耦合度,如下

    #二者看起来都像文件,因而就可以当文件一样去用,然而它们并没有直接的关系
    class Txt: #Txt类有两个与文件类型同名的方法,即read和write
        def read(self):
            pass
        def write(self):
            pass
    
    class Disk: #Disk类也有两个与文件类型同名的方法:read和write
        def read(self):
            pass
        def write(self):
            pass
    

    27、绑定方法与非绑定方法

    一 绑定方法与非绑定方法

    类中定义的函数分为两大类:绑定方法和非绑定方法

    其中绑定方法又分为绑定到对象的对象方法和绑定到类的类方法。

    在类中正常定义的函数默认是绑定到对象的,而为某个函数加上装饰器@classmethod后,该函数就绑定到了类。

    我们在之前的章节中已经介绍过对象方法了,本节我们主要介绍类方法。类方法通常用来在__init__的基础上提供额外的初始化实例的方式

    # 配置文件settings.py的内容
    HOST='127.0.0.1'
    PORT=3306
    
    # 类方法的应用
    import settings
    class MySQL:
        def __init__(self,host,port):
            self.host=host
            self.port=port
        @classmethod
        def from_conf(cls): # 从配置文件中读取配置进行初始化
            return cls(settings.HOST,settings.PORT)
    
    >>> MySQL.from_conf # 绑定到类的方法
    <bound method MySQL.from_conf of <class ‘__main__.MySQL'>>
    >>> conn=MySQL.from_conf() # 调用类方法,自动将类MySQL当作第一个参数传给cls
    

    绑定到类的方法就是专门给类用的,但其实对象也可以调用,只不过自动传入的第一个参数仍然是类,也就是说这种调用是没有意义的,并且容易引起混淆,这也是Python的对象系统与其他面向对象语言对象系统的区别之一,比如Smalltalk和Ruby中,绑定到类的方法与绑定到对象的方法是严格区分开的。

    二 非绑定方法

    为类中某个函数加上装饰器@staticmethod后,该函数就变成了非绑定方法,也称为静态方法。该方法不与类或对象绑定,类与对象都可以来调用它,但它就是一个普通函数而已,因而没有自动传值那么一说

    import uuid
    class MySQL:
        def __init__(self,host,port):
            self.id=self.create_id()
            self.host=host
            self.port=port
        @staticmethod
        def create_id():
            return uuid.uuid1()
    
    >>> conn=MySQL(127.0.0.1',3306)
    >>> print(conn.id) #100365f6-8ae0-11e7-a51e-0088653ea1ec
    
    # 类或对象来调用create_id发现都是普通函数,而非绑定到谁的方法
    >>> MySQL.create_id
    <function MySQL.create_id at 0x1025c16a8>
    >>> conn.create_id
    <function MySQL.create_id at 0x1025c16a8>
    

    总结绑定方法与非绑定方法的使用:若类中需要一个功能,该功能的实现代码中需要引用对象则将其定义成对象方法、需要引用类则将其定义成类方法、无需引用类或对象则将其定义成静态方法。

    28、反射、内置方法

    一 反射

    python是动态语言,而反射(reflection)机制被视为动态语言的关键。

    反射机制指的是在程序的运行状态中

    对于任意一个类,都可以知道这个类的所有属性和方法;

    对于任意一个对象,都能够调用他的任意方法和属性。

    这种动态获取程序信息以及动态调用对象的功能称为反射机制。

    在python中实现反射非常简单,在程序运行过程中,如果我们获取一个不知道存有何种属性的对象,若想操作其内部属性,可以先通过内置函数dir来获取任意一个类或者对象的属性列表,列表中全为字符串格式

    >>> class People:
    ...     def __init__(self,name,age,gender):
    ...         self.name=name
    ...         self.age=age
    ...         self.gender=gender
    ... 
    >>> obj=People('egon',18,'male')
    >>> dir(obj) # 列表中查看到的属性全为字符串
    [......,'age', 'gender', 'name']
    

    接下来就是想办法通过字符串来操作对象的属性了,这就涉及到内置函数hasattr、getattr、setattr、delattr的使用了(Python中一切皆对象,类和对象都可以被这四个函数操作,用法一样)

    class Teacher:
        def __init__(self,full_name):
            self.full_name =full_name
    
    t=Teacher('Egon Lin')
    
    # hasattr(object,'name')
    hasattr(t,'full_name') # 按字符串'full_name'判断有无属性t.full_name
    
    # getattr(object, 'name', default=None)
    getattr(t,'full_name',None) # 等同于t.full_name,不存在该属性则返回默认值None
    
    # setattr(x, 'y', v)
    setattr(t,'age',18) # 等同于t.age=18
    
    # delattr(x, 'y')
    delattr(t,'age') # 等同于del t.age
    

    基于反射可以十分灵活地操作对象的属性,比如将用户交互的结果反射到具体的功能执行

    >>> class FtpServer:
    ...     def serve_forever(self):
    ...         while True:
    ...             inp=input('input your cmd>>: ').strip()
    ...             cmd,file=inp.split()
    ...             if hasattr(self,cmd): # 根据用户输入的cmd,判断对象self有无对应的方法属性
    ...                 func=getattr(self,cmd) # 根据字符串cmd,获取对象self对应的方法属性
    ...                 func(file)
    ...     def get(self,file):
    ...         print('Downloading %s...' %file)
    ...     def put(self,file):
    ...         print('Uploading %s...' %file)
    ... 
    >>> server=FtpServer()
    >>> server.serve_forever()
    input your cmd>>: get a.txt
    Downloading a.txt...
    input your cmd>>: put a.txt
    Uploading a.txt...
    

    二 内置方法

    Python的Class机制内置了很多特殊的方法来帮助使用者高度定制自己的类,这些内置方法都是以双下划线开头和结尾的,会在满足某种条件时自动触发,我们以常用的__str__和__del__为例来简单介绍它们的使用。

    __str__方法会在对象被打印时自动触发,print功能打印的就是它的返回值,我们通常基于方法来定制对象的打印信息,该方法必须返回字符串类型

    >>> class People:
    ...     def __init__(self,name,age):
    ...         self.name=name
    ...         self.age=age
    ...     def __str__(self):
    ...         return '<Name:%s Age:%s>' %(self.name,self.age) #返回类型必须是字符串
    ... 
    >>> p=People('lili',18)
    >>> print(p) #触发p.__str__(),拿到返回值后进行打印
    <Name:lili Age:18>
    

    __del__会在对象被删除时自动触发。由于Python自带的垃圾回收机制会自动清理Python程序的资源,所以当一个对象只占用应用程序级资源时,完全没必要为对象定制__del__方法,但在产生一个对象的同时涉及到申请系统资源(比如系统打开的文件、网络连接等)的情况下,关于系统资源的回收,Python的垃圾回收机制便派不上用场了,需要我们为对象定制该方法,用来在对象被删除时自动触发回收系统资源的操作

    class MySQL:
        def __init__(self,ip,port):
            self.conn=connect(ip,port) # 伪代码,发起网络连接,需要占用系统资源
        def __del__(self):
            self.conn.close() # 关闭网络连接,回收系统资源
    
    obj=MySQL('127.0.0.1',3306) # 在对象obj被删除时,自动触发obj.__del__()
    

    29、元类

    一 元类介绍

    什么是元类呢?一切源自于一句话:python中一切皆为对象。让我们先定义一个类,然后逐步分析

    class StanfordTeacher(object):
        school='Stanford'
    
        def __init__(self,name,age):
            self.name=name
            self.age=age
    
        def say(self):
            print('%s says welcome to the Stanford to learn Python' %self.name)
    

    所有的对象都是实例化或者说调用类而得到的(调用类的过程称为类的实例化),比如对象t1是调用类StanfordTeacher得到的

    t1=StanfordTeacher('lili',18)
    print(type(t1)) #查看对象t1的类是<class '__main__.StanfordTeacher'>
    

    如果一切皆为对象,那么类StanfordTeacher本质也是一个对象,既然所有的对象都是调用类得到的,那么StanfordTeacher必然也是调用了一个类得到的,这个类称为元类

    于是我们可以推导出===>产生StanfordTeacher的过程一定发生了:StanfordTeacher=元类(…)

    print(type(StanfordTeacher)) # 结果为<class 'type'>,证明是调用了type这个元类而产生的StanfordTeacher,即默认的元类为type
    

    二 class关键字创建类的流程分析

    上文我们基于python中一切皆为对象的概念分析出:我们用class关键字定义的类本身也是一个对象,负责产生该对象的类称之为元类(元类可以简称为类的类),内置的元类为type

    class关键字在帮我们创建类时,必然帮我们调用了元类StanfordTeacher=type(…),那调用type时传入的参数是什么呢?必然是类的关键组成部分,一个类有三大组成部分,分别是

    1、类名class_name=‘StanfordTeacher’

    2、基类们class_bases=(object,)

    3、类的名称空间class_dic,类的名称空间是执行类体代码而得到的

    调用type时会依次传入以上三个参数

    综上,class关键字帮我们创建一个类应该细分为以下四个过程

    补充:exec的用法
    #exec:三个参数
    
    #参数一:包含一系列python代码的字符串
    
    #参数二:全局作用域(字典形式),如果不指定,默认为globals()
    
    #参数三:局部作用域(字典形式),如果不指定,默认为locals()
    
    #可以把exec命令的执行当成是一个函数的执行,会将执行期间产生的名字存放于局部名称空间中
    g={
        'x':1,
        'y':2
    }
    l={}
    
    exec('''
    global x,z
    x=100
    z=200
    
    m=300
    ''',g,l)
    
    print(g) #{'x': 100, 'y': 2,'z':200,......}
    print(l) #{'m': 300}
    

    四 自定义元类控制类StanfordTeacher的创建

    一个类没有声明自己的元类,默认他的元类就是type,除了使用内置元类type,我们也可以通过继承type来自定义元类,然后使用metaclass关键字参数为一个类指定元类

    class Mymeta(type): #只有继承了type类才能称之为一个元类,否则就是一个普通的自定义类
        pass
    
    # StanfordTeacher=Mymeta('StanfordTeacher',(object),{...})
    class StanfordTeacher(object,metaclass=Mymeta): 
        school='Stanford'
    
        def __init__(self,name,age):
            self.name=name
            self.age=age
    
        def say(self):
            print('%s says welcome to the Stanford to learn Python' %self.name)
    

    自定义元类可以控制类的产生过程,类的产生过程其实就是元类的调用过程,即StanfordTeacher=Mymeta(‘StanfordTeacher’,(object),{…}),调用Mymeta会先产生一个空对象StanfordTeacher,然后连同调用Mymeta括号内的参数一同传给Mymeta下的__init__方法,完成初始化,于是我们可以

    class Mymeta(type): #只有继承了type类才能称之为一个元类,否则就是一个普通的自定义类
        def __init__(self,class_name,class_bases,class_dic):
            # print(self) #<class '__main__.StanfordTeacher'>
            # print(class_bases) #(<class 'object'>,)
            # print(class_dic) #{'__module__': '__main__', '__qualname__': 'StanfordTeacher', 'school': 'Stanford', '__init__': <function StanfordTeacher.__init__ at 0x102b95ae8>, 'say': <function StanfordTeacher.say at 0x10621c6a8>}
            super(Mymeta, self).__init__(class_name, class_bases, class_dic)  # 重用父类的功能
    
            if class_name.islower():
                raise TypeError('类名%s请修改为驼峰体' %class_name)
    
            if '__doc__' not in class_dic or len(class_dic['__doc__'].strip(' \n')) == 0:
                raise TypeError('类中必须有文档注释,并且文档注释不能为空')
    
    # StanfordTeacher=Mymeta('StanfordTeacher',(object),{...})
    class StanfordTeacher(object,metaclass=Mymeta): 
        """
        类StanfordTeacher的文档注释
        """
        school='Stanford'
    
        def __init__(self,name,age):
            self.name=name
            self.age=age
    
        def say(self):
            print('%s says welcome to the Stanford to learn Python' %self.name)
    

    五 自定义元类控制类StanfordTeacher的调用

    储备知识:call

    class Foo:
        def __call__(self, *args, **kwargs):
            print(self)
            print(args)
            print(kwargs)
    
    obj=Foo()
    #1、要想让obj这个对象变成一个可调用的对象,需要在该对象的类中定义一个方法__call__方法,该方法会在调用对象时自动触发
    #2、调用obj的返回值就是__call__方法的返回值
    res=obj(1,2,3,x=1,y=2)
    

    由上例得知,调用一个对象,就是触发对象所在类中的__call__方法的执行,如果把StanfordTeacher也当做一个对象,那么在StanfordTeacher这个对象的类中也必然存在一个__call__方法

    class Mymeta(type): #只有继承了type类才能称之为一个元类,否则就是一个普通的自定义类
        def __call__(self, *args, **kwargs):
            print(self) #<class '__main__.StanfordTeacher'>
            print(args) #('lili', 18)
            print(kwargs) #{}
            return 123
    
    class StanfordTeacher(object,metaclass=Mymeta):
        school='Stanford'
    
        def __init__(self,name,age):
            self.name=name
            self.age=age
    
        def say(self):
            print('%s says welcome to the Stanford to learn Python' %self.name)
    
    
    
    # 调用StanfordTeacher就是在调用StanfordTeacher类中的__call__方法
    # 然后将StanfordTeacher传给self,溢出的位置参数传给*,溢出的关键字参数传给**
    # 调用StanfordTeacher的返回值就是调用__call__的返回值
    t1=StanfordTeacher('lili',18)
    print(t1) #123
    

    默认地,调用t1=StanfordTeacher(‘lili’,18)会做三件事

    1、产生一个空对象obj

    2、调用__init__方法初始化对象obj

    3、返回初始化好的obj

    对应着,StanfordTeacher类中的__call__方法也应该做这三件事

    class Mymeta(type): #只有继承了type类才能称之为一个元类,否则就是一个普通的自定义类
        def __call__(self, *args, **kwargs): #self=<class '__main__.StanfordTeacher'>
            #1、调用__new__产生一个空对象obj
            obj=self.__new__(self) # 此处的self是类OldoyTeacher,必须传参,代表创建一个StanfordTeacher的对象obj
    
            #2、调用__init__初始化空对象obj
            self.__init__(obj,*args,**kwargs)
    
            #3、返回初始化好的对象obj
            return obj
    
    class StanfordTeacher(object,metaclass=Mymeta):
        school='Stanford'
    
        def __init__(self,name,age):
            self.name=name
            self.age=age
    
        def say(self):
            print('%s says welcome to the Stanford to learn Python' %self.name)
    
    t1=StanfordTeacher('lili',18)
    print(t1.__dict__) #{'name': 'lili', 'age': 18}
    

    上例的__call__相当于一个模板,我们可以在该基础上改写__call__的逻辑从而控制调用StanfordTeacher的过程,比如将StanfordTeacher的对象的所有属性都变成私有的

    class Mymeta(type): #只有继承了type类才能称之为一个元类,否则就是一个普通的自定义类
        def __call__(self, *args, **kwargs): #self=<class '__main__.StanfordTeacher'>
            #1、调用__new__产生一个空对象obj
            obj=self.__new__(self) # 此处的self是类StanfordTeacher,必须传参,代表创建一个StanfordTeacher的对象obj
    
            #2、调用__init__初始化空对象obj
            self.__init__(obj,*args,**kwargs)
    
            # 在初始化之后,obj.__dict__里就有值了
            obj.__dict__={'_%s__%s' %(self.__name__,k):v for k,v in obj.__dict__.items()}
            #3、返回初始化好的对象obj
            return obj
    
    class StanfordTeacher(object,metaclass=Mymeta):
        school='Stanford'
    
        def __init__(self,name,age):
            self.name=name
            self.age=age
    
        def say(self):
            print('%s says welcome to the Stanford to learn Python' %self.name)
    
    t1=StanfordTeacher('lili',18)
    print(t1.__dict__) #{'_StanfordTeacher__name': 'lili', '_StanfordTeacher__age': 18}
    

    上例中涉及到查找属性的问题,比如self.new,请看下一小节

    五 再看属性查找

    结合python继承的实现原理+元类重新看属性的查找应该是什么样子呢???

    在学习完元类后,其实我们用class自定义的类也全都是对象(包括object类本身也是元类type的 一个实例,可以用type(object)查看),我们学习过继承的实现原理,如果把类当成对象去看,将下述继承应该说成是:对象StanfordTeacher继承对象Foo,对象Foo继承对象Bar,对象Bar继承对象object

    class Mymeta(type): #只有继承了type类才能称之为一个元类,否则就是一个普通的自定义类
        n=444
    
        def __call__(self, *args, **kwargs): #self=<class '__main__.StanfordTeacher'>
            obj=self.__new__(self)
            self.__init__(obj,*args,**kwargs)
            return obj
    
    class Bar(object):
        n=333
    
    class Foo(Bar):
        n=222
    
    class StanfordTeacher(Foo,metaclass=Mymeta):
        n=111
    
        school='Stanford'
    
        def __init__(self,name,age):
            self.name=name
            self.age=age
    
        def say(self):
            print('%s says welcome to the Stanford to learn Python' %self.name)
    
    
    print(StanfordTeacher.n) #自下而上依次注释各个类中的n=xxx,然后重新运行程序,发现n的查找顺序为StanfordTeacher->Foo->Bar->object->Mymeta->type
    

    于是属性查找应该分成两层,一层是对象层(基于c3算法的MRO)的查找,另外一个层则是类层(即元类层)的查找

    #查找顺序:
    #1、先对象层:StanfordTeacher->Foo->Bar->object
    #2、然后元类层:Mymeta->type
    

    依据上述总结,我们来分析下元类Mymeta中__call__里的self.__new__的查找

    class Mymeta(type): 
        n=444
    
        def __call__(self, *args, **kwargs): #self=<class '__main__.StanfordTeacher'>
            obj=self.__new__(self)
            print(self.__new__ is object.__new__) #True
    
    
    class Bar(object):
        n=333
    
        # def __new__(cls, *args, **kwargs):
        #     print('Bar.__new__')
    
    class Foo(Bar):
        n=222
    
        # def __new__(cls, *args, **kwargs):
        #     print('Foo.__new__')
    
    class StanfordTeacher(Foo,metaclass=Mymeta):
        n=111
    
        school='Stanford'
    
        def __init__(self,name,age):
            self.name=name
            self.age=age
    
        def say(self):
            print('%s says welcome to the Stanford to learn Python' %self.name)
    
    
        # def __new__(cls, *args, **kwargs):
        #     print('StanfordTeacher.__new__')
    
    
    StanfordTeacher('lili',18) #触发StanfordTeacher的类中的__call__方法的执行,进而执行self.__new__开始查找
    

    总结,Mymeta下的__call__里的self.new__在StanfordTeacher、Foo、Bar里都没有找到__new__的情况下,会去找object里的__new,而object下默认就有一个__new__,所以即便是之前的类均未实现__new__,也一定会在object中找到一个,根本不会、也根本没必要再去找元类Mymeta->type中查找__new__

    我们在元类的__call__中也可以用object.new(self)去造对象

    但我们还是推荐在__call__中使用self.new(self)去创造空对象,因为这种方式会检索三个类StanfordTeacher->Foo->Bar,而object.__new__则是直接跨过了他们三个

    最后说明一点

    class Mymeta(type): #只有继承了type类才能称之为一个元类,否则就是一个普通的自定义类
        n=444
    
        def __new__(cls, *args, **kwargs):
            obj=type.__new__(cls,*args,**kwargs) # 必须按照这种传值方式
            print(obj.__dict__)
            # return obj # 只有在返回值是type的对象时,才会触发下面的__init__
            return 123
    
        def __init__(self,class_name,class_bases,class_dic):
            print('run。。。')
    
    
    class StanfordTeacher(object,metaclass=Mymeta): #StanfordTeacher=Mymeta('StanfordTeacher',(object),{...})
        n=111
    
        school='Stanford'
    
        def __init__(self,name,age):
            self.name=name
            self.age=age
    
        def say(self):
            print('%s says welcome to the Stanford to learn Python' %self.name)
    
    
    print(type(Mymeta)) #<class 'type'>
    # 产生类StanfordTeacher的过程就是在调用Mymeta,而Mymeta也是type类的一个对象,那么Mymeta之所以可以调用,一定是在元类type中有一个__call__方法
    # 该方法中同样需要做至少三件事:
    # class type:
    #     def __call__(self, *args, **kwargs): #self=<class '__main__.Mymeta'>
    #         obj=self.__new__(self,*args,**kwargs) # 产生Mymeta的一个对象
    #         self.__init__(obj,*args,**kwargs) 
    #         return obj
    

    1、在元类中控制把自定义类的数据属性都变成大写

    class Mymetaclass(type):
        def __new__(cls,name,bases,attrs):
            update_attrs={}
            for k,v in attrs.items():
                if not callable(v) and not k.startswith('__'):
                    update_attrs[k.upper()]=v
                else:
                    update_attrs[k]=v
            return type.__new__(cls,name,bases,update_attrs)
    
    class Chinese(metaclass=Mymetaclass):
        country='China'
        tag='Legend of the Dragon' #龙的传人
        def walk(self):
            print('%s is walking' %self.name)
    
    
    print(Chinese.__dict__)
    '''
    {'__module__': '__main__',
     'COUNTRY': 'China', 
     'TAG': 'Legend of the Dragon',
     'walk': <function Chinese.walk at 0x0000000001E7B950>,
     '__dict__': <attribute '__dict__' of 'Chinese' objects>,                                         
     '__weakref__': <attribute '__weakref__' of 'Chinese' objects>,
     '__doc__': None}
    '''
    

    2、在元类中控制自定义的类无需__init__方法

    1.元类帮其完成创建对象,以及初始化操作;

    2.要求实例化时传参必须为关键字形式,否则抛出异常TypeError: must use keyword argument

    3.key作为用户自定义类产生对象的属性,且所有属性变成大写

    class Mymetaclass(type):
        # def __new__(cls,name,bases,attrs):
        #     update_attrs={}
        #     for k,v in attrs.items():
        #         if not callable(v) and not k.startswith('__'):
        #             update_attrs[k.upper()]=v
        #         else:
        #             update_attrs[k]=v
        #     return type.__new__(cls,name,bases,update_attrs)
    
        def __call__(self, *args, **kwargs):
            if args:
                raise TypeError('must use keyword argument for key function')
            obj = object.__new__(self) #创建对象,self为类Foo
    
            for k,v in kwargs.items():
                obj.__dict__[k.upper()]=v
            return obj
    
    class Chinese(metaclass=Mymetaclass):
        country='China'
        tag='Legend of the Dragon' #龙的传人
        def walk(self):
            print('%s is walking' %self.name)
    
    
    p=Chinese(name='lili',age=18,sex='male')
    print(p.__dict__)
    

    3、在元类中控制自定义的类产生的对象相关的属性全部为隐藏属性

    class Mymeta(type):
        def __init__(self,class_name,class_bases,class_dic):
            #控制类Foo的创建
            super(Mymeta,self).__init__(class_name,class_bases,class_dic)
    
        def __call__(self, *args, **kwargs):
            #控制Foo的调用过程,即Foo对象的产生过程
            obj = self.__new__(self)
            self.__init__(obj, *args, **kwargs)
            obj.__dict__={'_%s__%s' %(self.__name__,k):v for k,v in obj.__dict__.items()}
    
            return obj
    
    class Foo(object,metaclass=Mymeta):  # Foo=Mymeta(...)
        def __init__(self, name, age,sex):
            self.name=name
            self.age=age
            self.sex=sex
    
    
    obj=Foo('lili',18,'male')
    print(obj.__dict__)
    

    4、基于元类实现单例模式

    #步骤五:基于元类实现单例模式
    # 单例:即单个实例,指的是同一个类实例化多次的结果指向同一个对象,用于节省内存空间
    # 如果我们从配置文件中读取配置来进行实例化,在配置相同的情况下,就没必要重复产生对象浪费内存了
    #settings.py文件内容如下
    HOST='1.1.1.1'
    PORT=3306
    
    #方式一:定义一个类方法实现单例模式
    import settings
    
    class Mysql:
        __instance=None
        def __init__(self,host,port):
            self.host=host
            self.port=port
    
        @classmethod
        def singleton(cls):
            if not cls.__instance:
                cls.__instance=cls(settings.HOST,settings.PORT)
            return cls.__instance
    
    obj1=Mysql('1.1.1.2',3306)
    obj2=Mysql('1.1.1.3',3307)
    print(obj1 is obj2) #False
    
    obj3=Mysql.singleton()
    obj4=Mysql.singleton()
    print(obj3 is obj4) #True
    
    
    #方式二:定制元类实现单例模式
    import settings
    
    class Mymeta(type):
        def __init__(self,name,bases,dic): #定义类Mysql时就触发
            # 事先先从配置文件中取配置来造一个Mysql的实例出来
            self.__instance = object.__new__(self)  # 产生对象
            self.__init__(self.__instance, settings.HOST, settings.PORT)  # 初始化对象
            # 上述两步可以合成下面一步
            # self.__instance=super().__call__(*args,**kwargs)
            super().__init__(name,bases,dic)
    
        def __call__(self, *args, **kwargs): #Mysql(...)时触发
            if args or kwargs: # args或kwargs内有值
                obj=object.__new__(self)
                self.__init__(obj,*args,**kwargs)
                return obj
            return self.__instance
    
    class Mysql(metaclass=Mymeta):
        def __init__(self,host,port):
            self.host=host
            self.port=port
    
    obj1=Mysql() # 没有传值则默认从配置文件中读配置来实例化,所有的实例应该指向一个内存地址
    obj2=Mysql()
    obj3=Mysql()
    print(obj1 is obj2 is obj3)
    obj4=Mysql('1.1.1.4',3307)
    
    
    #方式三:定义一个装饰器实现单例模式
    import settings
    
    def singleton(cls): #cls=Mysql
        _instance=cls(settings.HOST,settings.PORT)
    
        def wrapper(*args,**kwargs):
            if args or kwargs:
                obj=cls(*args,**kwargs)
                return obj
            return _instance
        return wrapper
    
    
    @singleton # Mysql=singleton(Mysql)
    class Mysql:
        def __init__(self,host,port):
            self.host=host
            self.port=port
    
    obj1=Mysql()
    obj2=Mysql()
    obj3=Mysql()
    print(obj1 is obj2 is obj3) #True
    
    obj4=Mysql('1.1.1.3',3307)
    obj5=Mysql('1.1.1.4',3308)
    print(obj3 is obj4) #False
    

    30、异常处理

    一 什么是异常

    异常是程序发生错误的信号。程序一旦出现错误,便会产生一个异常,若程序中没有处理它,就会抛出该异常,程序的运行也随之终止。

    而错误分成两种,一种是语法上的错误SyntaxError,这种错误应该在程序运行前就修改正确

    >>> if  
      File "<stdin>", line 1
        if
         ^
    SyntaxError: invalid syntax
    

    另一类就是逻辑错误,常见的逻辑错误如

    # TypeError:数字类型无法与字符串类型相加
    1+2# ValueError:当字符串包含有非数字的值时,无法转成int类型
    num=input(">>: ") #输入hello
    int(num)
    
    # NameError:引用了一个不存在的名字x
    x
    
    # IndexError:索引超出列表的限制
    l=['egon','aa']
    l[3]
    
    # KeyError:引用了一个不存在的key
    dic={'name':'egon'}
    dic['age']
    
    # AttributeError:引用的属性不存在
    class Foo:
        pass
    Foo.x
    
    # ZeroDivisionError:除数不能为0
    1/0
    

    二 异常处理

    为了保证程序的容错性与可靠性,即在遇到错误时有相应的处理机制不会任由程序崩溃掉,我们需要对异常进行处理,处理的基本形式为

    try:
        被检测的代码块
    except 异常类型:
        检测到异常,就执行这个位置的逻辑
    

    举例

    try:
        print('start...')
        print(x) # 引用了一个不存在的名字,触发异常NameError
        print('end...')
    except NameError as e: # as语法将异常类型的值赋值给变量e,这样我们通过打印e便可以知道错误的原因
        print('异常值为:%s' %e)
    print('run other code...')
    
    #执行结果为
    start...
    异常值为:name 'x' is not defined
    run other code...
    

    如果我们想多种类型的异常统一用一种逻辑处理,可以将多个异常放到一个元组内,用一个except匹配

    try:
        被检测的代码块
    except (NameError,IndexError,TypeError):
        触发NameError或IndexError或TypeError时对应的处理逻辑
    

    举例

    def convert_int(obj):
        try:
            res=int(obj)
        except (ValueError,TypeError):
            print('argument must be number or numeric string')
            res=None
        return res
    
    convert_int('egon') # argument must be number or numeric string
    convert_int({'n':1}) # argument must be number or numeric string
    

    如果我们想捕获所有异常并用一种逻辑处理,Python提供了一个万能异常类型Exception

    try:
        被检测的代码块
    except NameError:
        触发NameError时对应的处理逻辑
    except IndexError:
        触发IndexError时对应的处理逻辑
    except Exception:
        其他类型的异常统一用此处的逻辑处理
    

    在多分支except之后还可以跟一个else(else必须跟在except之后,不能单独存在),只有在被检测的代码块没有触发任何异常的情况下才会执行else的子代码块

    try:
        被检测的代码块
    except 异常类型1:
        pass
    except 异常类型2:
        pass
    ......
    else:
        没有异常发生时执行的代码块
    

    此外try还可以与finally连用,从语法上讲finally必须放到else之后,但可以使用try-except-finally的形式,也可以直接使用try-finally的形式。无论被检测的代码块是否触发异常,都会执行finally的子代码块,因此通常在finally的子代码块做一些回收资源的操作,比如关闭打开的文件、关闭数据库连接等

    python try: 被检测的代码块 except 异常类型1: pass except 异常类型2: pass ...... else: 没有异常发生时执行的代码块 finally: 无论有无异常发生都会执行的代码块
    

    举例

    f=None
    try:
        f=open(‘db.txt’,'r',encoding='utf-8')
        s=f.read().strip()
        int(s)  # 若字符串s中包含非数字时则会触发异常ValueError
        # f.close() # 若上面的代码触发异常,则根本不可能执行到此处的代码,应该将关闭文件的操作放到finally中
    finally:
        if f: # 文件存在则f的值不为None
            f.close()
    

    在不符合Python解释器的语法或逻辑规则时,是由Python解释器主动触发的各种类型的异常,而对于违反程序员自定制的各类规则,则需要由程序员自己来明确地触发异常,这就用到了raise语句,raise后必须是一个异常的类或者是异常的实例

    class Student:
        def __init__(self,name,age):
            if not isinstance(name,str):
                raise TypeError('name must be str')
            if not isinstance(age,int):
                raise TypeError('age must be int')
    
            self.name=name
            self.age=age
    
    stu1=Student(4573,18) # TypeError: name must be str
    stu2=Student('egon','18') # TypeError: age must be int
    

    在内置异常不够用的情况下,我们可以通过继承内置的异常类来自定义异常类

    class PoolEmptyError(Exception): # 可以通过继承Exception来定义一个全新的异常
        def __init__(self,value='The proxy source is exhausted'): # 可以定制初始化方法
            super(PoolEmptyError,self).__init__()
            self.value=value
    
        def __str__(self): # 可以定义该方法用来定制触发异常时打印异常值的格式
            return '< %s >' %self.value
    
    
    class NetworkIOError(IOError): # 也可以在特定异常的基础上扩展一个相关的异常
        pass
    
    
    raise PoolEmptyError # __main__.PoolEmptyError: < The proxy source is exhausted >
    raise NetworkIOError('连接被拒绝') # __main__.NetworkIOError: 连接被拒绝
    

    最后,Python还提供了一个断言语句assert expression,断定表达式expression成立,否则触发异常AssertionError,与raise-if-not的语义相同,如下

    age='18'
    
    # 若表达式isinstance(age,int)返回值为False则触发异常AssertionError
    assert isinstance(age,int)
    
    # 等同于
    if not isinstance(age,int):
        raise AssertionError
    

    三 何时使用异常处理

    在了解了异常处理机制后,本着提高程序容错性和可靠性的目的,读者可能会错误地认为应该尽可能多地为程序加上try…except…,这其是在过度消费程序的可读性,因为try…except本来就是你附加给程序的一种额外的逻辑,与你的主要工作是没有多大关系的。

    如果错误发生的条件是“可预知的”,我们应该用if来进行”预防”,如下

    age=input('input your age>>: ').strip()
    if age.isdigit(): # 可预知只有满足字符串age是数字的条件,int(age)才不会触发异常,
        age=int(age)
    else:
        print('You must enter the number')
    

    如果错误发生的条件“不可预知”,即异常一定会触发,那么我们才应该使用try…except语句来处理。例如我们编写一个下载网页内容的功能,网络发生延迟之类的异常是很正常的事,而我们根本无法预知在满足什么条件的情况下才会出现延迟,因而只能用异常处理机制了

    import requests
    from requests.exceptions import ConnectTimeout # 导入requests模块内自定义的异常
    
    def get(url):
        try:
            response=requests.get(url,timeout=3)#超过3秒未下载成功则触发ConnectTimeout异常
            res=response.text
        except ConnectTimeout:
            print('连接请求超时')
            res=None
        except Exception:
            print('网络出现其他异常')
            res=None
        return res
    
    get('https://www.python.org')
    异常的实例
    
    ```python
    class Student:
        def __init__(self,name,age):
            if not isinstance(name,str):
                raise TypeError('name must be str')
            if not isinstance(age,int):
                raise TypeError('age must be int')
    
            self.name=name
            self.age=age
    
    stu1=Student(4573,18) # TypeError: name must be str
    stu2=Student('egon','18') # TypeError: age must be int
    

    在内置异常不够用的情况下,我们可以通过继承内置的异常类来自定义异常类

    class PoolEmptyError(Exception): # 可以通过继承Exception来定义一个全新的异常
        def __init__(self,value='The proxy source is exhausted'): # 可以定制初始化方法
            super(PoolEmptyError,self).__init__()
            self.value=value
    
        def __str__(self): # 可以定义该方法用来定制触发异常时打印异常值的格式
            return '< %s >' %self.value
    
    
    class NetworkIOError(IOError): # 也可以在特定异常的基础上扩展一个相关的异常
        pass
    
    
    raise PoolEmptyError # __main__.PoolEmptyError: < The proxy source is exhausted >
    raise NetworkIOError('连接被拒绝') # __main__.NetworkIOError: 连接被拒绝
    

    最后,Python还提供了一个断言语句assert expression,断定表达式expression成立,否则触发异常AssertionError,与raise-if-not的语义相同,如下

    age='18'
    
    # 若表达式isinstance(age,int)返回值为False则触发异常AssertionError
    assert isinstance(age,int)
    
    # 等同于
    if not isinstance(age,int):
        raise AssertionError
    

    三 何时使用异常处理

    在了解了异常处理机制后,本着提高程序容错性和可靠性的目的,读者可能会错误地认为应该尽可能多地为程序加上try…except…,这其是在过度消费程序的可读性,因为try…except本来就是你附加给程序的一种额外的逻辑,与你的主要工作是没有多大关系的。

    如果错误发生的条件是“可预知的”,我们应该用if来进行”预防”,如下

    age=input('input your age>>: ').strip()
    if age.isdigit(): # 可预知只有满足字符串age是数字的条件,int(age)才不会触发异常,
        age=int(age)
    else:
        print('You must enter the number')
    

    如果错误发生的条件“不可预知”,即异常一定会触发,那么我们才应该使用try…except语句来处理。例如我们编写一个下载网页内容的功能,网络发生延迟之类的异常是很正常的事,而我们根本无法预知在满足什么条件的情况下才会出现延迟,因而只能用异常处理机制了

    import requests
    from requests.exceptions import ConnectTimeout # 导入requests模块内自定义的异常
    
    def get(url):
        try:
            response=requests.get(url,timeout=3)#超过3秒未下载成功则触发ConnectTimeout异常
            res=response.text
        except ConnectTimeout:
            print('连接请求超时')
            res=None
        except Exception:
            print('网络出现其他异常')
            res=None
        return res
    
    get('https://www.python.org')
    
    展开全文
  • Python编程从入门到实践》———第一章

    千次阅读 多人点赞 2022-04-05 19:14:42
    书籍《Python编程从入门到实践》讲解

    《Python编程从入门到实践》———第一章

    目录

    1.1 搭建编程环境

    1.2 在Windows系统中搭建Python编程环境

    1.3 解决安装问题

    总结


    1.1 搭建编程环境

    环境构建,编程之始。

    Python的环境安装分为两部分:Python解释器和文本编辑器安装

    • 解释器:将代码翻译为机器认识的语言。(conda、Python)
    • 文本编辑器:我们用来撰写代码的工具,可以简化Python代码的编写工作,对初学者来说是个很方便的工具。(Pycharm、vscode、Sublime Text)

    1.2 在Windows系统中搭建Python编程环境

    良好的环境可以催发人的创造力

    安装解释器

    这里以安装Anaconda为例:附上Anaconda官网--------传送门

    1. 进入官网下载Anaconda,安装需注意以下几点

      • 需要知道自己的电脑是64位的还是32位的。
      • 如果是其他操作系统的电脑,点击下面获取其他安装程序就好了。
      • 安装路径不要保存到C盘哦!
      • 安装时务必选中复选框中的***Add Python to PATA,***这让你能够更轻松地配置系统。
        在这里插入图片描述
        在这里插入图片描述
    2. 启动Python终端会话,检查是否安装成功。

      • 在搜索栏中搜索cmd并打开,输入python检查是否成功安装。
        在这里插入图片描述

      • 若出现如上图一样的提示,那就说明你已经成功安装了。

    3. 在终端会话中运行Python

      • 成功安装Pyhton后,只需在步骤2后,继续在Pyhton会话中执行命令。如输入print(“hello world!”)

      在这里插入图片描述

    : 每当要运行Python代码片段时,都请打开一个命令窗口并启动Python终端会话。要关闭终端会话,可按Ctrl+Z,再按回车键。也可执行命令exit()。

    :终端是linux里的叫法,windows下面就为命令指示符(cmd)。

    安装文本编辑器

    这里以安装Pycharm为例:附上Pycharm官网--------传送门

    1. 进入官网下载Pycharm,安装需要注意以下几点

      • 这里所有的框全部勾选即可。
        在这里插入图片描述

      • 安装路径不要保存到C盘哦!

    在pycharm中运行Hello World程序

    1. 创建项目,配置解释器
      在这里插入图片描述

    在这里插入图片描述

    1. 新建Python文件

    在这里插入图片描述

    1. 运行Hello World程序!
      在这里插入图片描述

    1.3 解决安装问题

    如果你按前面的步骤做,应该能够成功地搭建编程环境。但如果你始终无法运行程序hello world.py,可尝试如下几个解决方案。

    1. 程序存在严重的错误时,Python将显示traceback。Python会仔细研究文件,试图找出其中的问题。traceback可能会提供线索,让你知道是什么问题让程序无法运行。
    2. 离开计算机,先休息一会,再尝试。别忘了,在编程中,语法十分重要,即便是少了一个冒号、引号不匹配或括号不匹配,都可能导致程序无法正确地运行。请再次阅读这篇博客的内容,再次审视你所做的工作,看看能否找出错误。
    3. 推倒重来。你也许不需要把一切推倒重来,但将文件hello world.py删除并重新创建它也许是个合理的选择。
    4. 让别人在你的计算机或其他计算机上按本章的步骤重做一遍,并仔细观察。你可能遗漏了一小步,而别人刚好没有遗漏。
    5. 请懂Python的人帮忙。当你有这样的想法时,可能会发现在你认识的人当中就有人使用Python。
    6. 到网上寻找帮助。如python社区,CSDN等等在线平台。

    1.4 在Windows系统中从终端运行Python程序

    1. 打开命令指示符

    2. 找到你储存hello world.py程序的文件,并单击右键打开属性,找到它所在的位置。

    在这里插入图片描述

    1. 注意文件夹所在的盘,若不为系统盘(C盘),则需要进行下面操作,切换到所在盘。

    在这里插入图片描述

    1. 使用终端命令==cd==在文件系统导航:cd 后面接文件夹所在位置即可
      在这里插入图片描述

    2. 使用命令==dir==列出当前目录中的所有文件
      在这里插入图片描述

    3. 运行其中的Python程序:python必不可少② .py 必不可少

      在这里插入图片描述

    1.5 小结

    在这章中,我们大致了解了什么是Python,并且学会了在自己的系统中配置Python环境,初步实现了在终端会话中运行Python代码片段以及了解了如何解决安装问题,自己动手解决问题。

    期待与小伙伴们第二章的学习!

    总结

    笔者的能力有限,许多地方没有能讲到,望大家见谅。如果大家对于这篇博客还有什么疑问或者什么高见的话,欢迎在评论区留言!

    展开全文
  • Python编程从入门到实践(基础入门)

    万次阅读 多人点赞 2019-05-31 19:13:42
    Python编程从入门到实践 1、Python中的变量 变量名:大小写英文,数字和下划线的组合,不能以数字开头 2、Python首字母大写使用title()方法,全部大写upper()方法,全部小写lower()方法 name=‘ce shi ...

    Python编程从入门到实践-------基础入门
    1、Python中的变量
    2、Python首字母大写使用title()方法,全部大写upper()方法,全部小写lower()方法
    3、Python中字符串拼接使用 + 号
    4、Python中删除字符串的空格 删除末尾空格的rstrip(),删除开头空格的lstrip(),删除两端空格的strip()
    5、Python2与python3的print区别:
    6、Python中整数运算,使用两个乘号表示乘方运算
    7、Python中避免类型错误使用str()函数,将其他类型值转化为字符串
    8、Python中的注释

    1、Python中的变量
    变量名:大小写英文,数字和下划线的组合,不能以数字开头


    2、Python首字母大写使用title()方法,全部大写upper()方法,全部小写lower()方法

    >>> name='ce shi python'
    >>> name.title();
    'Ce Shi Python'
    >>> print(name.upper())
    CE SHI PYTHON
    
    >>> name2="CE SHI PYTHON"
    >>> print (name2.lower())
    ce shi python
    

    3、Python中字符串拼接使用 + 号

    >>> first_name='ce'
    >>> last_name="shi"
    >>> full_name=first_name + last_name
    >>> print(full_name)
    ceshi
    >>> print("Hello"+" "+full_name+" "+'welcome!')
    Hello ceshi welcome!
    

    4、Python中删除字符串的空格 删除末尾空格的rstrip(),删除开头空格的lstrip(),删除两端空格的strip()

    >>> best_language=" python  "
    >>> best_language = best_language.rstrip()
    >>> best_language
    ' python'
    >>> best_language = best_language.lstrip()
    >>> best_language
    'python'
    
    >>> best_language=" python  "
    >>> best_language
    ' python  '
    >>> best_language.strip()
    'python'
    

    5、Python2与python3的print区别:
    python2中无需将要打印的内容放在括号内。python3中的print是一个函数,因此括号必不可少;有些python2的print也包含括号。


    6、Python中整数运算,使用两个乘号表示乘方运算

    >>> 3**2     #(=3*3)
    9
    >>> 10**3
    1000
    

    7、Python中避免类型错误使用str()函数,将其他类型值转化为字符串

    >>> age = 25
    >>> message = "Happy " + str(age) +" Birthday!"
    >>> message
    'Happy 25 Birthday!'
    >>> print(message)
    Happy 25 Birthday!
    

    8、Python中的注释
    以# 开头,后面的都是注释

    Python编程从入门到实践基础知识:https://blog.csdn.net/louzhu_lz/article/details/90721685
    Python编程从入门到实践(第三、四章的列表和元祖):https://blog.csdn.net/louzhu_lz/article/details/91354506
    Python编程从入门到实践(第五章if语句学习总结):https://blog.csdn.net/louzhu_lz/article/details/91409903
    Python编程从入门到实践(第六章字典学习总结):https://blog.csdn.net/louzhu_lz/article/details/91910554
    Python编程从入门到实践(第七章用户输入和while循环学习总结):https://blog.csdn.net/louzhu_lz/article/details/92384649
    Python编程从入门到实践(第八章函数)学习总结:https://blog.csdn.net/louzhu_lz/article/details/93377817

    展开全文
  • python编程从入门到实践-使用API
  • 经典的书籍:《python编程从入门到实践》的课后习题自写代码 + 官方文档 都是很简单的题,上传下当作自己的备份吧。。。
  • Python编程-从入门到实践》作者: Eric Matthes,已翻译为中文,人民邮电出版社出版。python编程从入门到实践怎么样?我们一起看看已经学习的同学对这本书的口碑和评价正面评价:《Python编程-从入门到实践》本书...
  • 1. 前言经常有读者问我,想让我推荐一些关于 Python 学习的书籍,鉴于每个人的知识背景以及跟我交流的深浅程度不一样,可能每次推荐的书籍都会有些差异。最近有些想法,打算陆陆续续介绍一...
  • 我用的编辑器是pycharm,然后安装第三方库pygame(功能很强大),这个项目代码还是比较多的,需要学习管理包含多个文件的项目,体会函数以及变量命名方法还需要重构很多代码,以提高代码的效率,总之,对于刚入门来...
  • 本书是一本针对所有层次的 Python 读者而作的 Python 入门书。全书分两部分 :第一部分介绍用 Python 编程所必须了解的基本概念,包括 matplotlib、NumPy 和 Pygal 等强大的 Python 库和工具介绍,以及列表、字典、...
  • 以下内容非标准答案,是我个人练习内容,仅供参考: 如有不准确,希望指出 # ~ #6-1 user_information = { 'first_name': '王', 'last_name': '小', 'age': 18, 'city': '中国上海' } print('姓: ' + user_...
  • python经典教材python编程从入门到应用第二部分
  • 今天给大家分享一本书 获取方式公众号后台回复Python基础获取百度网盘下载链接书籍简介本书旨在让你成为优秀的程序员,...在本书的第一部分,你将学习编写Python程序时需要熟悉的基本编程概念,你刚接触几乎任何编程...
  • Python编程从入门到实践##########
  • 刚开始自学python过程中,制作的思维导图,除去后面的三个项目,书中的基础部分基本涵盖。
  • 本书旨在让你成为优秀的程序员... 在本书的第一部分,你将学习编写Python程序时需要熟悉的基本编程概念,你刚接触几乎任何编程语言时 都需要学习这些概念。你将学习各种数据以及在程序中将数据存储列表和字典中的...
  • Python编程从入门到实践》习题答案及重点

    万次阅读 多人点赞 2019-09-09 14:31:06
    发现自己对于python的基础掌握的并不是很牢实,利用几天时间把重点写下来,并打算把(《Python编程从入门到实践》试一试)的代码全部敲一遍,如果有不对的地方,请不吝赐教。 目录 第1章 起步 第2章 变量和简单...
  • 【导语】Python编程从入门到实践是分两部分,第一部分是介绍用Python编程所必须了解的基本概念,第二部分是将理论付诸实践,两个部分分别包含的内容很多,那么Python编程从入门到实践学习内容包含哪些呢?接下来就...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 59,495
精华内容 23,798
关键字:

python编程从入门到实践