Loading... # 『闲谈』从不一样的角度谈Python入门 这是这个系列的第三篇文章,我终于用了一个中文标题(纯粹是懒得想了。 说到要学习一门编程语言,学习的动机之类的就先不考虑了。一个编程语言要学习的是什么——学习它的语法(syntax,有时也称作文法)以及如何用它(诸如最佳实践之类的)。按照这个思路,入门一些类似Python的语言真的不难。可以参照下图给出的编程语言关系图。 > 本文(或是说本系列)默认读者有计算机科学的学科基础。文中会给出一些由读者自行决定是否动手的交互式操作,通常使用分割线分隔这部分内容。为了照顾萌新,本文会尽量笼统地描述一些术语,与此同时,我会给出拓展文献以供部分人参考。  ## 引入 大家都说,Python是一个弱类型(week-typing)语言。弱类型这个表述,其实有很多歧义,下文会提到。入门前,我们要大致搞清楚,类型到底是什么。 首先,类型是一种对数据的抽象(abstract)。什么是抽象呢? 这也要从计算机的基础数据类型说起。之所以称之为基础,是因为这些数据类型能够较为简易的实现。比如其中大家都耳熟能详的整数类型(a.k.a. 整型),可以直接在寄存器或是内存中表示,且中央处理器(CPU)也会提供对应的运算器。与之类似的有浮点类型(float,或是双精度的double)。然后可以通过对字符(letter)进行编码(也就是把单个字符映射到一个表中),我们可以在支持整型的体系结构中直接实现字符类型,也就是说,C语言中的char的存储的是对应编码表的索引值(也叫下标),这个应该在计算机基础中有所提及。基于这些,我们也可以轻松实现字符串类型(string,Python中提供了关键字str)。byte存的是二进制(binary)数据这里就没必要解释了。本段简要说明的基础类型。 也就是说,类型将数据描述成了一个个字段(field,有时也称为property,属性)。 --- 我们来玩一玩Python。安装步骤略过,这里假定你已经安装了Python3.9且python3解释器位于`$PATH`中,如果你不明白上面的操作,请查看:[Python安装与使用](https://docs.python.org/3/using/index.html),如果使用需要编译安装的发行版,请自行查找环境变量配置教学(通常是修改`.bashrc`之类的文件)。左上角可以选择语言,选简体中文就好了。什么?你说你不知道哪个是简体中文?这里建议找个厂吧。 直接在终端(terminal)内输入`python3`进入交互式解释器,输入`class a: pass`并按两下回车,随后输入`a`打印它(在交互解释器中与`print(a)`等价)。我们会得到: ```python <class '__main__.a'> ``` 先别急,再输入`b = type('a', (), {})`并回车,然后打印`b`——跟上面的一模一样。 首先解释为什么输出会一模一样。Python是一个语言规范,是需要实现(implement)的。官方实现的解释器,也就是你能够从其[官网](https://python.org)下载的解释器,叫做CPython,也就是用C实现的Python解释器。这里给出CPython的类型对象(在Python中的对象叫做[`PyTypeObject`](https://docs.python.org/3/c-api/type.html#c.PyTypeObject),而在C实现中的对象叫做[_typeobject](https://docs.python.org/3/c-api/typeobj.html)。) ```cpp typedef struct _typeobject { PyObject_VAR_HEAD const char *tp_name; /* For printing, in format "<module>.<name>" */ Py_ssize_t tp_basicsize, tp_itemsize; /* For allocation */ ... } PyTypeObject; ``` 可见第3行的用于输出的`tp_name`,格式是`<模块>.<名称>`,而我们上面定义的两个类型都在解释器创建的`__main__`模块中,且名字相同。所以输出的结果也是相同的。 但是事实上,`a`与`b`并非相同的类型(虽然它们名字相同,且`type(a)==type(b)`,但这是因为`==`实现的原因。)我们可以通过构造一个`b`的实例`c`并使用`instanceof(c, a)`来判断。 ```python c = b() instanceof(c, a) ``` 结果为`False`。 --- 回到正题,上面的讲解是为了让读者能对Python的类型系统实现有初步的了解。类型描述数据,而将它作为变量(variable)的属性,就可以构造出变量在内存空间中的布局,并可以为开发者提供类型提示(type hint)。这是类型作为数据的抽象的一些作用。 再倒回去说一下弱类型这个描述,首先Python不提供变量名与变量类型的强制绑定,也就是说一个变量可以在初始化为某种数据类型时,后续被赋值为其它类型,而在一些静态类型(static typing)系统中,这种绑定一般是强制的(Rust提供了shadow语义来在某种意义上允许变量重新“赋值”)。其次,基于Python的运行时(runtime),一个对象(object)的属性可以动态修改(增加/删除)。也就是说,Python极大地弱化了类型的对代码编写的约束。但与此同时,带来的后果就是软件难以推断某个对象中是否拥有某个属性,在编译时不能检查某个对象是否存在某个属性,如果运行时属性不存在(或是不是一个Callable对象等),就会抛出异常,也就是我们所说的类型不安全。 简单来说,强类型系统可以让开发者心智负担更小,且不容易写出错误的代码。而弱类型系统可以提供更多的灵活性。当然,目前的大势也在富类型编程(type-rich)方向,也就是更多地使用类型来约束代码,提升代码的健壮性。 --- 随即,紧接上面的类型系统。Python内的所有东西,都是由`object`派生的。说人话,就是`object`是全部对象的爹(妈),这里的全部包括它(`object`)自己。以下代码可以验证: ```python >>> isinstance(object, object) True ``` 如果想要了解更多关于由`object`提供的默认实现,可以看看这个[Quick Reference](https://docs.python.org/3/c-api/typeobj.html#quick-reference)。这里就不过多展开了。 然后就可以介绍Python简单至极的语法了。这个大概就是一般意义上的入门吧。 ## 词法 讲真的,如果你好好学了编译原理或SICP其中任意一个玩意,你应该可以秒懂Python的词法。Python词法使用魔改版的*BNF*语法标记。它类似于这样定义Python的词法: ```bnf name ::= lc_letter (lc_letter | "_")* lc_letter ::= "a"..."z" ``` 能看懂的建议只需要大致阅读这个板块,然后去康康[Full Grammar specification](https://docs.python.org/3/reference/grammar.html)。 在这个板块,我可能会使用**词元(token,部分地方也称为形符)**这个有点奇怪的名词。过多地使用一些专业术语并非我本意,但是这个玩意真的太重要了。这个词本身可以简单理解成,用来表示代码中的每个独立元素的东西。更详细的定义可以参阅[词法分析#词元](https://en.wikipedia.org/wiki/Lexical_token)。 ### 行 Python中,以逻辑行分割代码(允许但不鼓励使用分号`;`)。逻辑行由一个或更多的物理行(可以支持多个平台的行分割符号:Windows`CR LF`、Linux `LF`、老式Macintosh ASCII 字符 `CR`)组成,逻辑行的判断遵循一些显式或隐式拼接规则。 ```python # 一行 a = 1 + 1 # 显式拼接使用 \,但是除非真的有必要,否则不推荐使用 \ 进行拼接 a = 1 \ + 1 # 使用括号的隐式拼接 a = ("123" .join('456')) # 这个代码只是为了展示"(" ")"的行拼接作用 # 使用方括号的隐式拼接 a = [ 1, 2 ,3, 4, 5, 6 ] # 使用大括号的隐式拼接 a = { 'a': 'xd', '123': '555', } ``` ### 注释 `#`在Python中为注释开头。具体用法见上方的代码。 ### 编码声明 从Python3.0开始,Python就按照[PEP-3120](https://www.python.org/dev/peps/pep-3120/)将*UTF-8*(一种支持多语言环境的字符编码标准)作为默认的源文件编码(encoding)了。但是有时候会有不使用*UTF-8*编码的情况,且在Python2时,默认的编码并非是*UTF-8*。所以Python提供了一个**编码声明**。 这个声明有点类似于`unix-like`环境下的*shebang(也称作hashbang)*,形如 ```bash #!/bin/bash ``` ```python #!/usr/bin/python3 ``` 这个玩意会被类Unix系统读取并调用该指令(前提是文件有执行权限)。而*shebang*必须位于文件的第一行(题外话,根据[PEP-394](http://www.python.org/dev/peps/pep-0394/),需要被直接执行的Python文件第一行应该为`#!/usr/bin/python`或`#!/usr/bin/python3`。)所以**编码声明**可以被放在*第一行或第二行*中的任意一行,但是其位于第二行时,第一行也必须是注释。该声明形如: ```python # -*- coding: <encoding-name> -*- # vim:fileencoding=<encoding-name> ``` 上面两种任意一种都可以,前者是Emacs支持的格式,后者是VIM支持的格式。 **拓展内容** 其实原理很简单,就是判断第一行或第二行为注释且匹配正则表达式`coding[=:]\s*([-\w.]+)`,匹配的结果就是文件的编码值。 ### 缩进 在Python中,代码层次使用缩进进行区分。就像*C like*语言中的大括号的作用一样。听好了,Python的层次是通过缩进数匹配的。也就是说你可以写出这样的代码: ```python def perm(l): # Compute the list of all permutations of l if len(l) <= 1: return [l] r = [] for i in range(len(l)): s = l[:i] + l[i+1:] p = perm(s) for x in p: r.append(l[i:i+1] + x) return r ``` 它组合使用了4空格缩进与8空格缩进,这本身没有什么问题。 但是,注意,按照定义,最外层代码块的第一行不能使用缩进。也就是说, ```python def perm(l): # 前面有两个空格 pass ``` 直接在最外层使用缩进是错误的——最外层的缩进必须为零值。 **拓展** >连续行的缩进层级以堆栈形式生成 INDENT 和 DEDENT 形符,说明如下。 > >读取文件第一行前,先向栈推入一个零值,该零值不会被移除。推入栈的层级值从底至顶持续增加。每个逻辑行开头的行缩进层级将与栈顶行比较。如果相等,则不做处理。如果新行层级较高,则会被推入栈顶,并生成一个 INDENT 形符。如果新行层级较低,则 *应当* 是栈中的层级数值之一;栈中高于该层级的所有数值都将被移除,每移除一级数值生成一个 DEDENT 形符。文件末尾,栈中剩余的每个大于零的数值生成一个 DEDENT 形符。 ### 标识符、特殊语义 标识符也称作名称。也就是说,它作为一个东西的名字,对这个东西起到标识(identity)作用。 在Python2中,仅支持包含ASCII范围内的字符标识符。跟很多语言一样,这里的标识符不以数字开头,仅由`A-Z a-z _ 0-9`组成。 而根据[PEP-3131](https://www.python.org/dev/peps/pep-3131),Python3中支持了Unicode范围内的字符。因此,这里的标识符可以由 - 大写字母 - 小写字母 - 词首大写字母 - 修饰符字母 - 其他字母 - 字母数字 - 以及由[PropList.txt](https://www.unicode.org/Public/13.0.0/ucd/PropList.txt)显示定义的*Other_ID_Start*字符列表 **开头**,注意这里面并不包含 - 非空白标识 - 含空白标识 - 十进制数字 - 连接标点 - 由[PropList.txt](https://www.unicode.org/Public/13.0.0/ucd/PropList.txt)显示定义的*Other_ID_Continue*字符列表 而后续字符可以使用上述字符。 标识符无长度限制,但是区分大小写。 还有一些基于下划线`_`提供的特殊类别语义。 `_*`:使用`from modulename import *`这种形式的导入语句,将不会引入具有这类名称的对象。 `__*__`:默认情况下,这种名称由解释器及其实现定义。并提供一些特殊语义。 `__*`: **拓展** 标识符的定义: ```bnf identifier ::= xid_start xid_continue* id_start ::= <all characters in general categories Lu, Ll, Lt, Lm, Lo, Nl, the underscore, and characters with the Other_ID_Start property> id_continue ::= <all characters in id_start, plus characters in the categories Mn, Mc, Nd, Pc and others with the Other_ID_Continue property> xid_start ::= <all characters in id_start whose NFKC normalization is in "id_start xid_continue*"> xid_continue ::= <all characters in id_continue whose NFKC normalization is in "id_continue*"> ``` 私有名称转换: > 当以文本形式出现在类定义中的一个标识符以两个或更多下划线开头并且不以两个或更多下划线结尾,它会被视为该类的 *私有名称*。 私有名称会在为其生成代码之前被转换为一种更长的形式。 转换时会插入类名,移除打头的下划线再在名称前增加一个下划线。 例如,出现在一个名为 `Ham` 的类中的标识符 `__spam` 会被转换为 `_Ham__spam`。 这种转换独立于标识符所使用的相关句法。 如果转换后的名称太长(超过 255 个字符),可能发生由具体实现定义的截断。 如果类名仅由下划线组成,则不会进行转换。 ### 关键字 也成为保留字,是提供一些语言内建(built-in)语义的通用方法。 这里给出关键字列表(Python3.9中的): ```python False await else import pass None break except in raise True class finally is return and continue for lambda try as def from nonlocal while assert del global not with async elif if or yield ``` 一样的,这里区分大小写。可以注意到仅有三个用于表示特殊值的关键字(`True False None`)开头为大写,其它均为全部小写。 ### 字面量 我们有时需要表示一些内建类型的常量,就像这样 ```python a = "foo" # str b = 1234 # int c = b'qwq' # byte ``` 等号后的表达式就是字面量。这里给出更加形式化的定义:字面值是内置类型常量值的表示法。 #### 字符串字面量合并 可以使用以下形式对字符串字面量进行合并: ```python a = "123"\ "456" ``` 无需显式的加号,Python会对两个以**空白符分割**的字符串字面量进行合并操作。 #### 格式化字符串 Python中,以`f-string`的形式提供这个语义: ```python >>> a = 123 >>> b = f"{{ 'a': {a} }}" >>> b "{ 'a': 123 }" >>> c = F"{a}.txt" >>> c '123.txt' >>> width = 10 >>> precision = 4 >>> value = decimal.Decimal("12.34567") >>> f"result: {value:{width}.{precision}}" # 嵌套 'result: 12.35' ``` 可以从上面看出,`f`/`F`均可。且两个大括号`{{`/`}}`会被替换成单个括号,用于转义。该表达式支持嵌套。 从Python3.8开始,支持`=`表达式,用法如下: ```python >>> foo = "bar" >>> f"{ foo = }" " foo = 'bar'" ``` #### 整数字面量 整数字面值的长度没有限制,能一直大到占满可用内存。 确定数值时,会忽略字面值中的下划线。下划线只是为了分组数字,让数字更易读。下划线可在数字之间,也可在 `0x` 等基数说明符后。 注意,除了 0 以外,十进制数字的开头不允许有零。以免与 Python 3.0 之前使用的 *C like* 八进制字面值混淆。 #### 浮点数字面量 在Python3.6及以后版本可以在浮点数字面量中使用下划线分组数字了。 Python中的浮点数,无论是整数部分还是指数部分,都以10为基数,也就是都仅使用十进制数字表示。 #### 虚数字面量 什么!Python竟然!(支持虚数! 震惊!…… 示例如下: ```python 3.14j 10.j 10j .001j 1e100j 3.14e-10j 3.14_15_93j ``` #### Ellipsis `...`为它的字面量,`Ellipsis`也是。这个字面量返回的值是一个单例对象。也就是说,`id(...)`是在运行时不变的。这样就可以放心使用`xxx is ...`这样的语句惹。 这个玩意没见过哪个地方提……就很迷惑。这玩意可以作为一个*magic value*,或是代替`pass`关键字,像这样: ```python def a(): ... ``` 或者在类型注解中代替`Any`。 数字类型字面量会隐式创建一个[`numbers.Number`](https://docs.python.org/zh-cn/3/library/numbers.html#numbers.Number)的子类。这类数字对象是不变的(Immutable),也就是说,对数字类型进行运算时,实在创建新的对象。 之所以给出这个板块,是为了让读者了解什么是字面量。 **拓展** 字符串字面量词法定义: ``` stringliteral ::= [stringprefix](shortstring | longstring) stringprefix ::= "r" | "u" | "R" | "U" | "f" | "F" | "fr" | "Fr" | "fR" | "FR" | "rf" | "rF" | "Rf" | "RF" shortstring ::= "'" shortstringitem* "'" | '"' shortstringitem* '"' longstring ::= "'''" longstringitem* "'''" | '"""' longstringitem* '"""' shortstringitem ::= shortstringchar | stringescapeseq longstringitem ::= longstringchar | stringescapeseq shortstringchar ::= <any source character except "\" or newline or the quote> longstringchar ::= <any source character except "\"> stringescapeseq ::= "\" <any source character> bytesliteral ::= bytesprefix(shortbytes | longbytes) bytesprefix ::= "b" | "B" | "br" | "Br" | "bR" | "BR" | "rb" | "rB" | "Rb" | "RB" shortbytes ::= "'" shortbytesitem* "'" | '"' shortbytesitem* '"' longbytes ::= "'''" longbytesitem* "'''" | '"""' longbytesitem* '"""' shortbytesitem ::= shortbyteschar | bytesescapeseq longbytesitem ::= longbyteschar | bytesescapeseq shortbyteschar ::= <any ASCII character except "\" or newline or the quote> longbyteschar ::= <any ASCII character except "\"> bytesescapeseq ::= "\" <any ASCII character> ``` 整数字面量词法定义: ```bnf integer ::= decinteger | bininteger | octinteger | hexinteger decinteger ::= nonzerodigit (["_"] digit)* | "0"+ (["_"] "0")* bininteger ::= "0" ("b" | "B") (["_"] bindigit)+ octinteger ::= "0" ("o" | "O") (["_"] octdigit)+ hexinteger ::= "0" ("x" | "X") (["_"] hexdigit)+ nonzerodigit ::= "1"..."9" digit ::= "0"..."9" bindigit ::= "0" | "1" octdigit ::= "0"..."7" hexdigit ::= digit | "a"..."f" | "A"..."F" ``` 浮点字面量词法定义: ```bnf floatnumber ::= pointfloat | exponentfloat pointfloat ::= [digitpart] fraction | digitpart "." exponentfloat ::= (digitpart | pointfloat) exponent digitpart ::= digit (["_"] digit)* fraction ::= "." digitpart exponent ::= ("e" | "E") ["+" | "-"] digitpart ``` 虚数字面量词法定义: ```bnf imagnumber ::= (floatnumber | digitpart) ("j" | "J") ``` ### 运算符 这里放个表: ```python + - * ** / // % @ << >> & | ^ ~ := < > <= >= == != ``` 在Python中,通过使用`__*__`特殊语义可以重载部分运算符。 ### 终结符 ```python ( ) [ ] { } , : . ; @ = -> += -= *= /= //= %= @= &= |= ^= >>= <<= **= ``` 均为半角符号。句点也可以用于浮点数与虚数字面量中。列表后半部分是增强赋值操作符,用作词法终结符,但也可以执行运算。 以下 ASCII 字符具有特殊含义,对词法分析器有重要意义: ``` ' " # \ ``` 以下 ASCII 字符不用于 Python。在字符串字面值或注释外使用时,将直接报错: ``` $ ? ` ``` --- 词法部分到这里就告一段落了。你可以略读这一部分,但是你应该对Python的词法分析器有了大概的认识。 ## 语法 最后修改:2021 年 04 月 04 日 03 : 37 AM © 允许规范转载 赞赏 请我喝杯咖啡 ×Close 赞赏作者 扫一扫支付 支付宝支付 微信支付
1 条评论
呆呆牛牛