(转)Python学习笔记系列——Python是一种纯粹的语言

  此文出自知乎灵剑,原文传送门:https://zhuanlan.zhihu.com/p/23926957

  在摸索适合自己的语言学习方法,看到一篇好文章,转之,侵删。

 

  Python的语法范式相当多、知识点相当细,但是Python是一种内在一致性很好的语言,理解了几条基本的规则,就很容易理解大部分语法与现象。本文会总结一下Python中最根本的几条规则,从这几条规则中,我们可以看出Python与许多其他语言不同,它是非常纯粹、非常和谐的。

Python的基本语法:表达式运算与名称绑定

Python的语句大体上可以分成简单语句和复合语句两类,简单语句当中,最重要的是表达式语句和赋值语句。为了区分C/C++等其他语言当中的赋值,本文中,我们将

a = 1
b = 'abc'
c = (a,b)

  这样的赋值语句称为名称绑定。顾名思义,它代表将右侧表达式运算的结果起了个名字,方便我们将来使用。相应的,变量在本文中称为名称,它主要代表名字本身,而不是对应的值。在官方文档当中叫做identifier(标识符),绑定也就是bound。

  关于表达式、简单语句、复合语句的分类,在官方文档  当中已经讲得非常好而全面了(用Python 3的注意左上角切换版本)。简单来说,表达式有很多种,比如常量,比如单个名称,比如运算,比如调用,比如yield等等,这个列表也会不断扩展。表达式的共同特点是它们都有且仅有一个返回值——有些返回值永远是None,有些返回值是元组,但可以认为都是有且仅有一个返回值。

  有些时候我们直接计算表达式就够了,比如有些函数调用。另一些时候,我们会关心这个表达式求出的值,我们希望在后面重新使用这个计算出的值,我们就用到了名称绑定,也就是为这个结果起一个方便我们后面使用的名字,通过名称绑定,它是非常浅显易懂的语法,通常由一个等号组成,等号左侧是要赋值的名称,右侧是表达式,表达式的结果就会绑定到这个名称上:

<target> = <expression>

这种包含等号的语句叫做赋值语句,它还有另外几种变种:

<target1>, <target2> = <sequence-with-length-2>
# 如:
a,b = (1,2)
# 或者
a,b = 1,2

表示解构的赋值

<target1> = <target2> = <target3> = ... = <expr>

可以将同一个值绑定到多个名称。

赋值语句和名称绑定略有区别,它除了用于名称绑定以外,还可以用来修改可变对象如列表(list)和字典(dict):

a[1] = 2
a[2:3] = [4,5,6]
b['a'] = 12

通常需要注意区别。

Python绑定 vs C赋值

我们看下面的代码:

a = 1
a = "abc"

  当我们对已经绑定了的名称重新赋值的时候,旧的值会解除绑定,新的值绑定到名称,这对于旧的值和新的值来说都是不可见的。名称相当于相应值的别名,这个值有多少个名称,都不会影响到值本身;同样,不同的名称也都代表同一个值,因此如果多个名称绑定到了同一个值,而值发生了变化,所有名称对应的值都会变化:

a = [1,2]
b = a
a.append(3)
a # [1,2,3]
b # [1,2,3]

  以往会用“传值/传引用”、“强类型/弱类型”这样的概念,对于Python来说其实都是不必要的,我们记住这样的规则:名称只是对象的别名,不同的名称可以表示同一个对象,同一个对象并没有唯一的名称。

C中的变量就完全不一样,一旦定义就占用固定的内存,赋值实际上是实际内存值的复制。Python的名称是个逻辑概念,而C的变量相当贴近物理实现,这也体现了Python作为编程语言更为纯粹这一特点。

除了重新绑定时候自动解除旧值的绑定,Python中的名称还可以强制解除绑定:

del <identifier>

特殊绑定

  除了赋值语句以外,还有一些特殊语句会实现名称绑定,主要包括import、def(函数定义)、class(类定义)。此外,某些语句的主要功能不是名称绑定,但可以额外附加名称绑定的效果,如for、except、with。

重点说前三个,它们实际上都是赋值语句的直接变形,如import:

import my_module
import my_module as m1
from my_module import my_object
from my_module import my_object as o1

实际上完全等效于:

my_module = __import__('my_module')

m1 = __import__('my_module')

_ = __import__('my_module')
my_object = _.my_object
del _

_ = __import__('my_module')
o1 = _.my_object
del _

  这些语句只是将表达式求值 + 赋值的过程写成了比较统一、简洁、易懂的形式,要理解它们本质上仍然是赋值语句。因此import的结果也可以用del解除绑定,可以重新绑定到新的值。这体现了Python语言的一致性。

定义函数和类也是这样:

def my_func():
    pass
#重新绑定
my_func = 123
#解除绑定
del my_func

这与C/C++区别很大,它们都是在定义的时候创建了函数/类的对象,然后将这个对象绑定到了相应的名称上,理解这一点对于理解嵌套函数定义之类的规则大有帮助。

作用域,作用域嵌套与global作用域

在函数中,名称可以独立进行重新绑定,而不影响外部绑定的效果,这种特性叫做名称作用域:

a = 1
def func():
    a = 2
    return a
func() # 2
a # 1

每个函数都有自己独立的作用域,但与C/C++不同,复合语句没有自己独立的作用域。函数中可以嵌套定义其他函数,这些函数也有自己的作用域:

a = 1
def func1():
    a = 2
    def func2():
        a = 3
        return a
    return (a, func2())
func1() # (2,3)
a # 1

最外层的作用域叫做global作用域,对于一个最外层的函数来说,这是这个函数定义的模块。 

名称可以同时存在在多个不同的作用域中,在Python的函数中使用名称时,名称解析到具体哪一个作用域,服从下面的规则:

  1. 一旦这个函数中存在对某个名称的绑定,则在这个函数中,名称会解析到本函数的作用域
  2. 否则,依次考虑更高一层的作用域,直到这个名称会被解析到这个作用域(或者说,在这一级作用域中,存在对这个名称的绑定) ,或者到达global作用域为止。

“存在对某个名称的绑定”,包括赋值语句,也包括之前提到的特殊绑定,以及之后会提到的参数绑定。注意只要存在这样的绑定的语句,哪怕这个语句从未执行,也会导致作用域解析的变化,换句话来说,名称解析到哪个作用域,是函数定义的时候就决定了的:

a = 2
def func():
    if False:
        a = 1
    return a   # UnboundLocalError

然而,具体的解析到值的过程,则是在语句运行时才进行的,这个需要着重注意:

def func():
    a = 1
    def func2():
        return a
    a = 2
    return func2()

func()    # 2

初学者经常犯的错误就是这样的:

def multipliers(n):
    funcs = []
    for i in range(0,n):
        def _func(b):
            return i * b
        funcs.append(_func)
    return funcs

ms = multipliers(10)
ms[2](3)   # 27, 9 * 3

正确理解了其中的原理之后,这个现象就一点也不难理解了:i在运行时才解析,而运行时,循环已经结束了,所以i是9

每个函数都有自己的global作用域,它是函数定义的Python文件(或者说模块)。它实际上是模块命名空间与__builtins__命名空间两层,其中__builtins__对所有的模块都生效。

参数绑定

对于函数来说,函数的参数的绑定是比较特殊的一个过程,它在调用的时候进行绑定,但是是将传入的参数值,在函数自己的作用域中,绑定到参数名称:

a = 1
def func(a):
    return a
func(2) # 2
a # 1

它服从一切名称绑定的作用域规则,可以重新绑定,可以del,可以被子作用域解析。

参数可以有默认值,有默认值的情况下,可以理解为实际的参数绑定之前,预先将这些名称绑定到了对应的值,如果调用时指定了新值则重新绑定到新值。

如果既没有默认值、也没有在调用时绑定,就会发生名称未绑定的错误。

小结

  • Python可以运行表达式,也可以将表达式的值绑定到某个名称
  • 绑定的形式有普通绑定,特殊绑定,参数绑定
  • 绑定与作用域的规则:
    • 默认绑定到当前作用域
    • 解析名称时,会解析到最近一层绑定该名称的作用域
    • global/nonlocal可以修改这个特性
    • 定义时决定解析到哪个作用域;运行时决定解析的值 

前面这么多内容,整理成规则却非常简洁,有没有感受到Python的确是很协调、很纯粹的语言呢?

后记

本文是从面向初学者的一个演示文稿中整理而来,这是前一部分,后一部分讲面向对象。演示文稿当中的一部分更基础的内容,考虑到受众原因没有在这篇文章中提到。

理解Python的规则的另一种方法是了解对象的内部构造,比如code对象内部有哪些成员,cellvars和freevars各代表什么,__closure__属性以及实现,对象的__dict__与属性的关系,等等,有一定编程基础的也推荐这种学习方式。

posted @ 2018-11-26 16:23  zzz紫川  阅读(347)  评论(0编辑  收藏  举报