0
点赞
收藏
分享

微信扫一扫

Python基础教程(下)

读思意行 2022-03-16 阅读 60

Python – Life is pathetic, let’s pythonic

以下内容多半来自官方文档,使用解释型语言是一件和享受服务类似的事情——而且你可以更方便地实现复杂的抽象。不过这不是本文的要点,本文的要点如下:

概览

  • 首要:标识和注释
  • 交互模式:简单数据处理
  • 定义过程:简单流程控制
  • 模块编程:函数,对象,模块
  • 更多的细节:迭代器和生成器
  • 甜蜜的诱惑:语法糖
  • 结余:评价Python

首要

Python使用#进行单行注释,解释器不会对一行代码里#后面的内容进行求值(除非这个#是字符串里的#)。使用三个连续的引号(单双均可)作为多行注释开始和结束的标识符。

有一句很常见的python文件开头的注释:

#!/usr/bin/python

写过shell脚本的人一眼就明白,这是一句shell脚本。#!是shell脚本用的标记,它指明这个脚本需要什么解释器来执行,例如#!/bin/sh表示用解释器/bin/sh来执行此脚本。/usr/bin/是类unix系统下的一个路径,python解释器的可执行程序默认放在这个地方,这样,在shell下直接运行python脚本的时候,它给shell指明python解释器路径。这样在shell下就可以这样运行脚本a.py:(与python a.py等效,很鸡肋的操作,不是吗?我并不推荐你这么做)

./a.py

而如果直接用python解释器运行脚本,第一行就又变成了一句python注释。在powershell里,你却不能这么做,因为powershell调用解释器使用的是&,开头不是#的话,就没法用python解释器去运行脚本了。不过,windows下一般都有PATHONPATH这个注册表项,你只要双击对应的脚本,它就会通过这个注册表项找到你电脑上的python解释器运行。因此,windows用户们就忘记这句注释吧。

交互模式

大多数解释器支持在终端下直接运行,交互模式主要用在shell调用之类的场合。倒也不失为一种快速展示功能的方式。Python解释器的界面默认是这样的:

>>>def foo():
...    print('foo')
...

开头的提示符>>>...并不是什么神奇的东西,它们由变量sys.ps1和sys.ps2定义,可以修改成我们想要的其它符号。ipython是一个更强大的python交互式环境,准确的说,repl(read-eval-print loop)环境。尽管ipython没能打败shell,但只要稍稍花点时间了解,用起来是很爽的。

Python的语法吸收了shell脚本的特点,不需要像C里面的;这种语句分隔符。对于太过冗长的行,可以在行尾加\符进行折行。如果想在一行里写很多句,使用分号作为结束符依然是允许的——但不是被提倡的。单个分号不能作为“空语句”,应该使用直白的pass语句。相关的代码规范参见pep8标准,如果你平时在IDE里会开一个pep8的lint,你很快就能适应这种规范。

Python的算术操作和C基本一致。Python有计算乘方的内置函数,使用**运算符即可。C里面的除号在Python里是//(floor divition)符,它会对结果截取整数部分,而/总是返回浮点数,这么做或许是必要的——试想,假如我希望得到一个精确的浮点数结果,但是参与计算的都是整数却又不能整除,在C里面会触发自动的类型转换,因为左值一定在某处声明了是浮点类型,在Python里,因为它是弱类型,我们应该尽可能地不对这些原始类型做类型声明和显式的类型转换,为了达到这一点,若返回值是整型,接受的变量也必须变成整型,这就又造成不得不显式转换类型的困扰了。

Python继承了C里面“包含浮点数的运算默认返回浮点数”的上溢机制。如果要初始化一个Python变量的类型属性,一个简单的方式是直接给它指定一个对应类型的右值(如果用Python做C风格的声明,它可能不会有实际的内存)。Python也保留了C的原子类型,它还支持比C更广泛的变量类型——但是,那些也可以看作是附加的算术类,在此不提。Python内置类之间的强制类型转换采用的是全局函数,例如:

float(x) #将x转换成浮点型
int("aa55",16) # 将字符串aa55作为16位整数读取为一个整型

在后面我们将会理解为何python在很多地方可以使用全局函数。

Python的移位、比较运算符与C相同,允许按字典序比较列表。逻辑运算符则使用直观的andorxor(或许是来源于Perl),实际上C的逻辑运算符也是被支持的——不过,逻辑非不能用单个!表示,只有!=仍表示“不等于”,~用于按位取反。

Python3.8有一个新的特性是海象运算符:=,有人说这是是融合ST语言的结果。海象符的引入是备受争议的,一个重要因素可能是,这个符号与Python的其它机制显得格格不入。它的作用是给一个赋值表达式返回值,C的=本来就具有此功能,但是Python的=是一个很不同的函数,而且它的句法又不是函数式句法,所以这个运算符应该只在方便的时候使用,杜绝滥用。

Python中的字符串也和C相似,它支持单双引号两种标识方式,与C一样,采用\作为转义字符,如果一个字符串里有较多\不需要转义,你可以在它前面用r表示不进行转义,repr函数有相同的效果。

Python的字符串常量支持C风格的直接拼接,不过更为简洁的方式是用单/双引号三连,没错,就是变成一个字符串常量,相当于C的宏字符串,你可以用行尾加\的方式合并块中的两行。这也是换行符的本质,它实际上叫做行拼接符。

字符串变量可以用运算符+来拼接,用*来重复。字符串可以用下标进行索引,Python索引的特点是下标可以是负数,负数下标对应的是从最后一个元素倒着数。切片是Python处理列表的强大操作,就字符串而言,它的作用类似C艹 std::string::substr(),可以截取一个子串,做浅拷贝,并不会拷贝实际的内存。

有趣的是,这里的上下界索引依然可以是负数,而且不分大小端;步长也很随意,甚至可以自由越界,然而最终一定可以截取出一个有效字符串。Python里面的字符串属于immutable(有人译为“不可变的”)对象,你不能对它或者它的元素进行赋值操作——这也说明字符串变量仅仅是智能指针,它们所使用的内存资源是自动分配和回收的。

列表(或者说Python序列)是一种与数组不同的抽象结构——尽管它的底层实现一般依赖于数组。在一门解释型语言里,用户一般不需要关注数据结构在底层是怎样被实现的,因而列表是一种纯粹的数据抽象,而不像数组,一个C数组大多数时候都对应着一块连续的内存,而不是一个抽象的空间。列表比数组更适合描述复杂的抽象结构。

Python序列结构分为两类,可变的(mutable)的对象被称为列表(list),允许用户对它的内容进行修改,作为参数时传递的是引用;而不可变的(immutable)的对象被称为元组(tuple),内容是只读的,作为参数时被操作的是对拷贝的引用。字符串常量属于后者。列表以[]为标识符,元组以()为标识符,用逗号分隔列表元素(此处有一个“丑陋”的地方,那就是只有一个元素的元组必须有一个逗号才能声明它的元组类型,否则它会被当作一个变量),用list()函数可以将元组拷贝为列表。

为什么会有元组呢?我觉得这或许是单纯的adhoc,首先,元组在存储上可以是连续的,因为不可更改。而且在查找等方面,Python对元组等类型做了优化,但是列表没有优化。也就是说,我们需要一个有类似功能的东西来提高运行效率。

对一维列表(元组)的操作和字符串变量基本一致,列表提供一套操作原子(即列表的元素)对象的方法,迭代器,脚本操作符和语法糖(参见后面)。大多数Python库也提供提供队列和栈之类的基于序列的基本容器,尽管它们都可以用列表实现。Python提供的另外两种内置的序列类型是集合和字典,它们本质上是特殊的列表,前者(关键字set)提供自动检测重复的方法,后者(关键字dict)提供查表的方法。Python里没有switch-case,它的分支多选是由表驱动的——相比起与branch指令关系密切的switch,使用表进行检索无疑是更方便和有条理的。

定义过程

Python中的基本过程和shell差不多,没有花括号,它的过程开始标识是’:’,以空行为结束标识——毕竟,大多数时候我们都会留个空行,既然如此,为什么不直接以空行为标识呢?Python在多行代码的风格上是个完美主义者(似乎这是很多人所称道的),同一级过程里的每行代码必须保持相同的首行缩进——而这一点大多数的编辑器都可以自动提供,我也认为这实在是Python的高明之处。到现在为止,既然一门独立的,完全依赖于编辑器的编程工具尚不存在,那么能利用好编辑器提供的种种便利的编程语言就是最优秀的。说句题外话,编程语言到底是数学家们搞出来的东西,“编程”这项任务需要的只是工具,未必要依托于打字机,我想发起一个开发脱离语言的编程工具的计划。(nai)

闲话不提,回到Python的过程,Python支持和C一样的基本的控制流程,即ifforwhile结构,也有breakcontinue控制语句。其中if子句采用的是elif的写法。else子句貌似用处很广泛,它可以在异常处理中使用。Python异常块使用的是C艹风格的try...except捕获,我一直觉得这样的设计是鸡肋,对Python而言更是如此。在try...except的后面可以跟一个else,它里面放try块没有异常被捕获的时候应该执行的代码。而无论如何是否都要在最后执行的代码可以用finally声明。else还有一个妙用,那就是可以与循环搭配省区一个if。比如下面的例子:

>>> for n in range(2, 10):
...     for x in range(2, n):
...         if n % x == 0:
...             print(n, 'equals', x, '*', n//x)
...             break
...     else:
...         print(n, 'is a prime number')
...

当循环未触发break时就会触发后面的else语句,这与未触发except时执行的else子句差不多。

值得关注的是Python的for结构,它也是表驱动的,而不是使用同步的循环,这种函数式的设计对效率不是一件好事,如果要在遍历的同时对列表做改动,往往只能拷贝一份列表,对这个副本进行修改。与数字相关的迭代往往借助range()实现,它基于我们所期望的序列返回连续的项,但并不会创造对应的列表,这样的对象称为可迭代的(iterable),它可以节省一部分资源。

模块编程

作为一项传统也许我们应该先学会说helloworld,好吧,我们直接公布答案,python向标准输出进行打印使用的是print函数。Python函数的调用看上去与C函数没什么不同,填满参数就可以调用,然后接收返回值(即使不使用return返回一个值,它也有一个固定的返回值叫做None)。有人说Python里面一切都是对象(官方也这么说),不过,这里的对象显然不能用理解C艹对象的方法去理解,因为Python里的类也是对象——我们暂时更感兴趣的是与参数传递和调用约定相关的问题。

Python函数用def作为声明的标识,函数声明的形状和C一样,包括函数名和放在括号里的参数,可以在声明处指明参数的默认值,可以在f.__defaults__里查看f的默认参数值

Python函数具有一定的类性质,可适用类的成员访问操作符。函数声明实际上是在全局符号表里添加了一个包含默认值信息的函数符号表,而每一处本地调用都将实参(的引用)填进一个列表,用此列表创建一个用来存储局部变量的本地符号表。f.__defaults__里的值只初始化一次,如果是mutable类型的参数,相当于拥有了一个固定的全局指针值,这样每次都会指向同一块内存,达到类似静态变量的效果——难怪说Python函数也是对象,创建函数本地符号表的过程就像类的实例化过程一样。

函数查找变量的顺序是先本地后全局,正因为函数参数被当作列表处理,函数参数的个数实际上不固定,你可能给一个函数传入更多的参数,参数位置往往也不唯一。为了澄明这些,在函数声明里可以使用/*分隔出不同类型的参数。/之前的参数被称为Positional only的,它可以被叫做位置参数,位置参数必须按参数声明里的参数顺序传入,和C的调用语法差不多;*之后的部分被称为Keyword only的,可以被叫做关键字参数,关键字参数按照关键字名称对应传入的参数,例如f(a=1,b="hello")是使用关键字参数对f(b,a)进行调用的一种方式。函数默认参数属于关键字参数。二者之间的参数则是二者皆可用,默认按位置参数处理,但也允许使用关键字。而欲使用不限定个数的参数则可以使用以*args结尾作为可变参数的标志,显然,在*args之后补充的参数只能是Keyword only的,即便将*args放到/之前,它被建议放在参数列表的最后面并且不应该被滥用,因为可变参数很可能被忽略。列表也可以成为参数的来源,前缀*可以将列表(或元组)解包成位置参数,前缀**则可以将字典解包成关键字参数。python函数的返回使用return语句,不需要显式声明,可以返回多个值,其原理和参数是一致的。

Python的函数调用规则乍一看很混乱,归根结底是为了保证用户不需要关注原子类型,而关注列表形式——然而对于函数调用,调用者很难只凭名称了解函数的参数应该是什么样子——这时候文档和注释的重要性就体现出来了,在def语句下面紧跟的注释段会被记为函数的__doc__属性。函数的参数相关信息可以用:附在参数后面,像注释一样,通过f.__annotations__可以查看,默认为空。Python还支持定义简单的lambda表达式——它其实更像是宏,主要起简化参数传递的作用。

Python支持定义和使用类。从之前对函数的了解我们可以看出,Python的函数既不是完全依赖列表的,也不是单纯过程化的,而是对象化的。事实上,考虑def关键字的功能,嵌套的def完全是可行的,这就是对函数封装的简单实现。而声明一个类要提供一种可以产生实例的工厂,用class关键字即可提供这个功能。

Python类的构造通过定义一个叫做__init__的方法完成(实际上,__init__只负责初始化,类的创建是隐式调用__new__完成的)。__new__的第一个参数是所谓的元类,也就是所有类的类,用户创建的类是元类的实例。可以通过调用有三个参数的type()直接创建一个类。

实例的析构通过__del__完成,显式调用del可以清除全局对象。与之前的运算符底层函数相似,很多类专有方法格式都是__xxx__,例如__add__提供对’+'的重载,这好像来自C艹的底层代码。python所有的类方法都是虚函数,可以在子类中重写。大多数Python代码遵循这样一个约定:以一个下划线开始的名称应该被当作是API的非公有部分,如果开头是两个下划线,这个名称就被解释为私有的,这种机制叫做name mangling(官方文档译为“名称改写”)。

通过这种机制,我们可以利用这些私有的名字给一个作用域绑定私定义的方法。与原子对象一样,类的实例化一般通过=赋值动作完成——尽管显式的类型声明仍然是允许的。Python类方法对实例自身的引用需要额外的参数传递,需要在函数声明里显式放置第一个参数self(可以起其它的名字,但是习惯用self),这是不太讨人喜欢的地方。一个python对象(或者类实例)只多了一个功能,那就是引用具体的属性。实例化的类属性也都会被实例化,方法实例可以通过__self__属性查看它作为实例的具体地址,而__func__属性可以查看它在类定义中的地址。(在C语言里,这两个地址一般都是同一个)类继承通过括号进行,多重继承自左向右构造父类,子类可以通过super()方法可以调用父类。

懂得了如何定义函数和类就可以开始模块编程了。一件值得从一开始就学习的事情是如何写函数和类的文档。官方给出的金规矩是这样的:文档字符串第一行之后的第一个非空行确定整个文档字符串的缩进量,保持这个缩进量,缩进更少的行不应该出现,如果它们出现,则应该保证它们的缩进量是标准缩进量的倍数,而且不依赖空格。应在转化制表符为空格后测试空格的等效性(通常转化为8个空格)。这样,只要使用print(f.__doc__)就可以查看函数f的文档了。关于代码风格,PEP8是官方推荐的一种风格。懂得如何保持优良的代码风格是走进模块编程的第一步。

第二步则是要了解何为名字空间(尽管同样是namespace一词,但是它的作用与C艹命名空间有差别)。大部分的名字空间都是通过Python字典来实现的,Python用名字管理对象,名字空间规定了名字和对象之间的映射关系,它的相关信息包含在我们之前提到过的符号表里。

在不同时刻创建的名字空间有不同的生存周期。包含内置名称的名字空间是在Python解释器启动时创建的,这个内置名字空间在Python解释器运行过程中始终存在。模块的全局名字空间在模块定义被读入时创建,它通常也会持续到解释器退出。一个函数的局部名字空间在这个函数被调用时创建,在函数返回,或中途向上抛出异常时被删除。

在一个名字空间里可访问的Python代码被称为作用域,变量的生命周期和作用域是我们必须关注的两个要素。以下是命名空间可被直接访问的嵌套作用域:

  • 局部名字空间
  • 函数传递闭包
  • 全局名字空间
  • 内置名字空间

所谓闭包是一个形式语言的概念,形式语言L的闭包L*具有这样的特性,闭包中的任何一个字串都可以由某个初始字串按形式文法推导得到。Python允许一个函数返回另一个函数,这样的函数的名字空间是不能轻易删除的。因为最后一个被返回的函数就像初始字串,返回它的函数,返回返回它的函数的函数,…都可以由调用顺序推出来,这些函数的名字空间最后会形成一个闭包,闭包内函数的名字空间和它返回的函数的名字空间具有相同的生命周期。

globalnonlocal可以声明特定变量生存于对应的作用域中并且应当在其中被重新绑定。Python的声明与赋值同步有时是很恼人的事情,因为我们只能通过赋值去做类似变量声明的事情。赋值的本质是调用了构造,这也成就了Python的对象化,而Python对象的作用域虽然静态预定,却是动态执行的——这对不良的类设计是种魔鬼的宽容。

所谓的模块是一个包含Python定义和语句的文件,每个Python脚本都可以称为模块。在一个模块内部,模块名(作为一个字符串)可以通过全局变量__name__的值获得,我们用解释器运行某个模块的时候,它的__name__就会被赋值为__main__,也就是当前运行的主模块。模块可以包含可执行的语句以及函数定义。这些语句用于初始化模块。它们仅在模块第一次import进将运行的模块或者被直接调用。大概来自Modula-3,import是Python模块之间相互引用的关键字,import和C的include关键字差不多,不过更加简便灵活。它可以将一个模块引入另一个模块,或者将一个模块里的一些符号引入另一个模块的符号表(用dir()即可查询模块内定义的名称)。python解释器将从PYTHONPATH和当前目录下寻找我们import进去的模块。实际上,引入过的模块一般会在缓存目录__pycache__下保存一个编译过的版本(.pyc/),它里面是编译过的机器码,因而解释器对它们的解析速度更快。

多个模块往往组织在一个包里,所谓的包其实就是一个目录结构,用’.'即可查找到包目录下具体的模块,这样的组织方式实际上构造了一个包命名空间。包具有__path__属性,它的功能与sys.path基本相同,在导入期间提供一个包含搜索模块位置的列表。对一个包导入*并不会导入全部模块,而是导入__all__定义的模块,或者只起到一句“确保它能被导入”的作用。如果要导入兄弟包或者父包,则可以使用from.和from…来指示当前目录和父目录。这里不太好的地方是对.有滥用之嫌。..本身是当前目录和父目录的标识,在使用包命名空间的时候应该避开使用.,哪怕这会打乱某种队形——只能说Python是个完美主义者吧。

更多的细节

在遍历过程中我们用到的一类重要对象是迭代器。Python的名称改写机制很容易实现迭代器泛型对对象的绑定。Python迭代器协议由返回迭代器对象的__iter__和返回容器中下一项的__next__组成。Python支持定义异步迭代器,它可以在async for中使用,它的协议对应__aiter____anext__

我们在之前并没有提到函数是什么样的对象,显然,函数是没有元函数的,Python函数是基于下层对象创建的实例。一类重要的下层对象是生成器,它有四个基本方法,__next__sendthrowclose。生成器基于生成器表达式,yield表达式是Python用来定义生成器的表达式。当一个生成器函数被调用的时候,它返回一个被称为生成器的迭代器,这个迭代器控制生成器函数的执行。当这个迭代器的某一个方法被调用的时候,生成器函数开始执行,一直执行到第一个yield表达式前,在此执行被挂起,给生成器的调用者返回expression_list的值。挂起后,我们说所有局部状态都被保留下来,包括局部变量的当前绑定,指令指针,内部求值栈和任何异常处理的状态。调用迭代器的某一个方法触会发生成器函数继续执行。此时函数的运行就和yield表达式只是一个外部函数调用的情况完全一致。恢复后yield表达式的值取决于调用的哪个方法来恢复执行。如果用的是__next__()(通常通过语言内置的for或是next()来调用)那么结果就是None,如果用send()那么结果就是传递给send方法的值。使用async关键字可以定义异步生成器函数,用于async for

Python进行系统编程也相当简便,在此不提。

甜蜜的诱惑

Python有一个神奇的多重赋值功能,例如,swap(a,b)在Python里可以写成:

a,b = b,a

要理解多重赋值,就要明白=的右值是如何被处理的,实际上,Python解释器会将所有的右值作为一个元组,赋值是对此元组进行所谓的序列解包的过程,只要是可以被视为序列的类型都能被序列解包。

还有一些功能本身不是原始设计所必需的,但是它们相当友好简便,这样的功能就被称为语法糖(syntactic sugar)。例如Python支持形如0<x<1的逻辑判别式,以及装饰器的@写法。列表的切片操作本身也是语法糖。

另一个有趣的功能是内嵌的遍历表达式,例如

a=[[1,2,3],[4,5],[6],[7,8],[9]]
[i for k in a for i in k]

# 上述列表等价于:
i=[]
for k in a:
    for l in k:
        i.append(l)

上面的语句生成了一个列表,这种定义列表的语法有时被叫做列表解析(或者列表推导),其实这并不是什么特殊的定义机制,也并不是Python的专利。任何一项功能算不算语法糖并没有定论,你觉得甜就可以叫它糖,不是嘛?

结余

Python的流行一开始让我感到难以置信,如果说C艹是具有实用主义精神,python可以说具有“即插即用”精神。它的确有一些东西是‘好’的,例如取消了很多冗余机制,它在外观上追求美感,在使用上追求简便,但是,Python毫无工业美感可言,它对差劲的设计没有排斥力,它的坏处正是它好处的副作用:Python不给使用者洞察自己设计的机会。

然而正所谓大道至简,编程语言不仅仅是语言。

by gynamics

以前写的东西现在看真是好笑,如果对您有帮助真是再好不过了。

举报

相关推荐

0 条评论