介绍Python中一些重要的基本概念与机制,包括物理行与逻辑行、模块与包、字面值与变量、参数传递、lambda函数、作用域与命名空间。
物理行与逻辑行
Python语言参考手册 - 词法分析 - 行结构 本章内容需要一点点 编译原理-词法分析器 的知识。文档中 “形符” 对应英语 Token,可以理解为词法分析器产生的、给解释器看的特殊符号,代码中不可见。
物理行 是一序列字符,由行尾序列终止。源文件和字符串可使用任意标准平台行终止序列 - Unix ASCII 字符 LF (换行)、 Windows ASCII 字符序列 CR LF (回车换行)、或老式 Macintosh ASCII 字符 CR (回车)。不管在哪个平台,这些形式均可等价使用。输入结束也可以用作最终物理行的隐式终止符。
NEWLINE 形符表示结束 逻辑行。语句不能超出逻辑行的边界,除非句法支持 NEWLINE (例如,复合语句中的多行子语句)。根据显式或隐式 行拼接 规则,一个或多个物理行可组成逻辑行。
两个及两个以上的物理行可用反斜杠(\)拼接为一个逻辑行。此为 显式行拼接。例如:
if 1900 < year < 2100 and 1 <= month <= 12 \
and 1 <= day <= 31 and 0 <= hour < 24 \
and 0 <= minute < 60 and 0 <= second < 60: # Looks like a valid date
return 1
注意:以反斜杠结尾的行,不能加注释;反斜杠也不能拼接注释。
圆括号、方括号、花括号内的表达式可以分成多个物理行,不必使用反斜杠。 此为 隐式行拼接。例如:
month_names = ['Januari', 'Februari', 'Maart', # These are the
'April', 'Mei', 'Juni', # Dutch names
'Juli', 'Augustus', 'September', # for the months
'Oktober', 'November', 'December'] # of the year
或者,似乎有点神奇的情况:
if (1900 < year < 2100 and 1 <= month <= 12 # Looks like a valid date
and 1 <= day <= 31 and 0 <= hour < 24
and 0 <= minute < 60 and 0 <= second < 60):
return 1
隐式行拼接可含注释;后续行的缩进并不重要;还支持空的后续行。
模块与包
关于模块与包的详细说明可以参考:Python 教程 - 6.模块 、 Python 语言参考手册 - 5. 导入系统 。
module – 模块:此对象是 Python 代码的一种组织单位。各模块具有独立的命名空间,可包含任意 Python 对象。模块可通过 importing 操作被加载到 Python 中。
package – 包:一种可包含子模块或递归地包含子包的 Python module。从技术上说,包是带有
__path__
属性的 Python 模块。
可以把包看成是文件系统中的目录,并把模块看成是目录中的文件,但请不要对这个类比做过于字面的理解,因为包和模块不是必须来自于文件系统。
最常见的包是一个带有 __init__.py
文件的目录,——这个文件可以没有任何内容。这是在Python3.2及以前就存在的 “常规包”。常规包被导入时,__init__.py
文件会隐式地被执行,它所定义的对象会被绑定到该包命名空间中的名称。
例如下面的目录结构,hello是包,而demo只是模块:
hello\
__init__.py
demo.py
>>> import hello.demo
>>> hello.__path__
['C:\\Users\\user\\PyProjects\\hello']
>>> hello.demo.__path__
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: module 'hello.demo' has no attribute '__path__'
>>> hello.demo.__package__
'hello'
.py
文件可以被python命令直接执行,也可以带 -m
参数执行。以下举例说明两者的区别。
目录结构如下:
hello\
subpack1\
__init__.py
my_module.py
my_module.py 内容:
import sys
if __name__ == "__main__":
print("__spec__ : " + repr(__spec__))
print("sys.path : " + sys.path)
执行结果:
C:\Users\user\PyProjects>python hello\subpack1\my_module.py
__spec__ : None
sys.path : ['C:\\Users\\user\\PyProjects\\hello\\subpack1', 'C:\\Users\\user\\AppData\\Local\\Programs\\Python\\Python310\\python310.zip', ...]
C:\Users\user\PyProjects>python -m hello.subpack1.my_module
__spec__ : ModuleSpec(name='hello.subpack1.my_module', loader=<_frozen_importlib_external.SourceFileLoader object at 0x00000175A22D9430>, origin='C:\\Users\\user\\PyProjects\\hello\\subpack1\\my_module.py')
sys.path : ['C:\\Users\\user\\PyProjects', 'C:\\Users\\user\\AppData\\Local\\Programs\\Python\\Python310\\python310.zip', ...]
可见,带 -m
参数执行时,__spec__
不再是 None
,且 path
的第一个值变为了当前执行命令所在目录。
再看看导入了其他模块的情况。
增加子包 subpack2 及 my_classses.py 文件:
hello\
subpack1\
__init__.py
my_module.py
subpack2\
__init__.py
my_classes.py
修改 my_module.py:
import sys
from ..subpack2.my_classes import A
if __name__ == "__main__":
print("__spec__ : " + repr(__spec__))
print("sys.path : " + str(sys.path))
a = A()
a.foo1(100)
执行结果
C:\Users\user\PyProjects>python -m hello.subpack1.my_module
__spec__ : ModuleSpec(name='hello.subpack1.my_module', loader=<_frozen_importlib_external.SourceFileLoader object at 0x000001EC921B9430>, origin='C:\\Users\\user\\PyProjects\\hello\\subpack1\\my_module.py')
sys.path : ['C:\\Users\\user\\PyProjects', 'C:\\Users\\user\\AppData\\Local\\Programs\\Python\\Python310\\python310.zip', ...]
foo1(100) 被调用.
C:\Users\user\PyProjects>python hello\subpack1\my_module.py
Traceback (most recent call last):
File "C:\Users\user\PyProjects\hello\subpack1\my_module.py", line 2, in <module>
from ..subpack2.my_classes import A
ImportError: attempted relative import with no known parent package
字面值与变量
Python中有个 “字面值(Literals)” 的概念:
Literals are notations for constant values of some built-in types.
字面值是某些内置类型常量值的表示法。
字面值包括:字符串/字节串字面值、数值字面值(整数字面值、浮点数字面值和虚数字面值)。
字面值是常量,不能被修改。某些操作只能适用于字面值。
下面的例子中,str1
、str2
、str3
都是字符串变量,而 "Hello "
、"world."
是字符串字面值。其中,对 str3
赋值时采用的字符串拼接方式只能用于字符串字面值。
>>> str1 = "Hello "
>>> str2 = str1 + "world."
>>> str3 = "Hello " "world."
>>> print(str2)
Hello world.
>>> print(str3)
Hello world.
参数传递
命令行参数
https://docs.python.org/zh-cn/3/tutorial/interpreter.html#argument-passing
解释器读取命令行参数,把脚本名与其他参数转化为字符串列表存到 sys 模块的 argv 变量里。执行 import sys,可以导入这个模块,并访问该列表。该列表最少有一个元素;未给定输入参数时,sys.argv[0] 是空字符串。给定脚本名是 ‘-’ (标准输入)时,sys.argv[0] 是 ‘-’。使用 -c command 时,sys.argv[0] 是 ‘-c’。如果使用选项 -m module,sys.argv[0] 就是包含目录的模块全名。解释器不处理 -c command 或 -m module 之后的选项,而是直接留在 sys.argv 中由命令或模块来处理。
举例说明:
# argv_demo.py
import sys
if __name__ == "__main__":
i = 0
for v in sys.argv:
print("argv[" + str(i) + "]: " + v)
i += 1
C:\Users\user\PyProjects\hello>python argv_demo.py
argv[0]: argv_demo.py
C:\Users\user\PyProjects\hello>python argv_demo.py 10 foo=bar
argv[0]: argv_demo.py
argv[1]: 10
argv[2]: foo=bar
使用VSCode调试时传递命令行参数
在 Python笔记 - 开发环境搭建 - 安装并配置IDE 这部分讲了用VSCode调试单个python文件,但更多时候我们需要调试的是一个拥有很多模块的包,或者我们想要向模块中传递命令行参数,这就需要以文件夹方式打开python工程。还是以上面的argv_demo.py为例。下面截图就不解释了。
函数参数
关于函数定义详解,可查看官方教程:https://docs.python.org/zh-cn/3/tutorial/controlflow.html#more-on-defining-functions
默认值参数:调用时可以省略默认值。
>>> def ask_ok(prompt, retries=4, reminder='Please try again!'):
... while True:
... ok = input(prompt)
... if ok in ('y', 'ye', 'yes'):
... return True
... if ok in ('n', 'no', 'nop', 'nope'):
... return False
... retries = retries - 1
... if retries < 0:
... raise ValueError('invalid user response')
... print(reminder)
...
>>> ask_ok('Do you really want to quit?')
Do you really want to quit?yes
True
>>> ask_ok('Do you really want to quit?', 2)
Do you really want to quit?no
False
>>> ask_ok('OK to overwrite the file?', 2, 'Come on, only yes or no!')
OK to overwrite the file?d
Come on, only yes or no!
OK to overwrite the file?y
True
>>>
关键字参数:调用时可以改变函数定义时的参数顺序。
>>> def foo(arg0, arg1='DEFAULT_1', arg2='DEFAULT_2', arg3='DEFAULT_3'):
... print('arg0: ' + arg0)
... print('arg1: ' + arg1)
... print('arg2: ' + arg2)
... print('arg3: ' + arg3)
...
>>> foo('something')
arg0: something
arg1: DEFAULT_1
arg2: DEFAULT_2
arg3: DEFAULT_3
>>> foo('something', 'good')
arg0: something
arg1: good
arg2: DEFAULT_2
arg3: DEFAULT_3
>>> foo('something', arg2='good')
arg0: something
arg1: DEFAULT_1
arg2: good
arg3: DEFAULT_3
>>> foo('something', arg2='good', arg1='really')
arg0: something
arg1: really
arg2: good
arg3: DEFAULT_3
传递任意个数的参数
最后一个形参为 **name
形式时,接收一个字典(Dict),该字典包含与函数中已定义形参对应之外的所有关键字参数。**name
形参可以与 *name
形参组合使用(*name
必须在 **name
前面), *name
形参接收一个 元组(Tuple),该元组包含形参列表之外的位置参数。【字典和元组在前面讲 for 循环时提到过】
看下面例子:
def func(arg0, *args, **kwargs):
print('arg0: ' + arg0)
print('-- args ' + '-' * 30)
for v in args:
print(v)
print('-- kwargs ' + '-' * 28)
for k,v in kwargs.items():
print(k + ': ' + v)
>>> func('Colors:', 'Pink', 'Orange', 'Grey', mode='RGB | CMYK', selected='RGB')
arg0: Colors:
-- args ------------------------------
Pink
Orange
Grey
-- kwargs ----------------------------
mode: RGB | CMYK
selected: RGB
func()
的调用方式也可以改为下面这样,效果完全一样。
>>> my_args = ('Pink', 'Orange', 'Grey')
>>> my_kwargs = {'mode': 'RGB | CMYK', 'selected': 'RGB'}
>>> func('Colors:', *my_args, **my_kwargs)
lambda函数
https://docs.python.org/zh-cn/3/tutorial/controlflow.html#lambda-expressions
lambda 关键字用于创建小巧的匿名函数。lambda a, b: a+b 函数返回两个参数的和。Lambda 函数可用于任何需要函数对象的地方。在语法上,匿名函数只能是单个表达式。在语义上,它只是常规函数定义的语法糖。
def foo(x, y):
return (x * y) ** 2
if __name__ == '__main__':
z1 = foo(3, 4)
print(z1)
# 等效于foo()
bar = lambda x, y: (x * y) ** 2
z2 = bar(3, 4)
print(z2)
作用域与命名空间
官方教程关于作用域与命名空间的介绍在这里:https://docs.python.org/zh-cn/3/tutorial/classes.html#python-scopes-and-namespaces
但是官方文档的顺序是先讲抽象概念,再举例说明。个人觉得反着看可能更容易理解。
作用域(scope)
先看下面这个例子(在官方示例基础上略有改动):
#scope_test.py
def scope_test():
print('---- scope_test begin ' + '-' * 33)
def do_local():
# 本地变量
spam = "local spam"
print("spam in do_local(): ", spam)
def foo():
def do_nonlocal():
# 非本地变量(包裹函数变量)
nonlocal spam
spam = "nonlocal spam"
print("spam in do_nonlocal(): ", spam)
spam = "foo spam"
print("Before do_nonlocal assignment: ", spam)
do_nonlocal()
print("After do_nonlocal assignment: ", spam)
def do_global():
# 模块全局变量
global spam
spam = "global spam"
print("spam in do_global(): ", spam)
spam = "scope_test spam"
print("Before local assignment:", spam)
do_local()
print("After local assignment:", spam)
print('-' * 40)
print("Before foo():", spam)
foo()
print("After foo():", spam)
print('-' * 40)
print("Before do_global assignment:", spam)
do_global()
print("After do_global assignment:", spam)
print('---- scope_test end ' + '-' * 35)
spam = "module spam"
if __name__ == '__main__':
print("Before scope_test():", spam)
scope_test()
print("After scope_test():", spam)
执行结果:
Before scope_test(): module spam
---- scope_test begin ---------------------------------
Before local assignment: scope_test spam
spam in do_local(): local spam
After local assignment: scope_test spam
----------------------------------------
Before foo(): scope_test spam
Before do_nonlocal assignment: foo spam
spam in do_nonlocal(): nonlocal spam
After do_nonlocal assignment: nonlocal spam
After foo(): scope_test spam
----------------------------------------
Before do_global assignment: scope_test spam
spam in do_global(): global spam
After do_global assignment: scope_test spam
---- scope_test end -----------------------------------
After scope_test(): global spam
可以看到,do_nonlocal()
改变了它的包裹函数 foo()
里面定义的变量 spam
,do_global()
改变了模块的全局变量 spam
。
对上面的程序略作改动,删除第19行 spam = "foo spam"
,再次执行后,foo()
的结果发生了改变。
----------------------------------------
Before foo(): scope_test spam
Before do_nonlocal assignment: scope_test spam
spam in do_nonlocal(): nonlocal spam
After do_nonlocal assignment: nonlocal spam
After foo(): nonlocal spam
----------------------------------------
因为 nonlocal
会逐级往上查找 spam
,直到找到为止。
那如果把 spam = "scope_test spam"
也删除呢?do_nonlocal()
会修改模块的全局变量吗?结果是得到下面的异常:
File "c:\Users\user\PyProjects\demo\scope_test.py", line 15
nonlocal spam
^
SyntaxError: no binding for nonlocal 'spam' found
上面的例子演示了三个作用域:本地作用域、包裹函数作用域、模块全局作用域。Python 里还有第四个作用域:包含 built-in 名字的最外层作用域。
- the innermost scope, which is searched first, contains the local names
- 最内层作用域,包含局部名称,并首先在其中进行搜索
- the scopes of any enclosing functions, which are searched starting with the nearest enclosing scope, contains non-local, but also non-global names
- 封闭函数的作用域,包含非局部名称和非全局名称,从最近的封闭作用域开始搜索
- the next-to-last scope contains the current module’s global names
- 倒数第二个作用域,包含当前模块的全局名称
- the outermost scope (searched last) is the namespace containing built-in names
- 最外层的作用域,包含内置名称的命名空间,最后搜索
命名空间(namespace)
简单说,命名空间就是名称到对象的映射。
A namespace is a mapping from names to objects.
当前,大多数命名空间都实现为Python字典(不过以后可能会发生变化)。内置函数集合、内置异常名称、模块的全局名称、函数的局部名称、对象的属性集合都是命名空间。
命名空间是在不同时刻创建的,且拥有不同的生命周期。
- built-in 的命名空间是在 Python 解释器启动时创建的,永远不会被删除。
- 模块的全局命名空间在读取模块定义时创建;通常,模块的命名空间也会持续到解释器退出。
- 函数的本地命名空间在调用该函数时创建,并在函数返回或抛出不在函数内部处理的异常时被删除。
- 每次递归调用都会有自己的本地命名空间。
名称与对象(name and object)
Objects have individuality, and multiple names (in multiple scopes) can be bound to the same object. This is known as aliasing in other languages.
对象之间相互独立,多个名称(在多个作用域内)可以绑定到同一个对象。 其他语言称之为别名。
- 赋值不会复制数据,只是将名称绑定到对象。
- 删除也是如此:语句
del x
从局部作用域引用的命名空间中移除对x
的绑定,而不是删除x
绑定的对象。 - 在调用函数时,会将实际参数(实参)引入到被调用函数的局部符号表中;因此,实参是使用
按值调用
来传递的(其中的值
始终是对象的引用
而不是对象的值)。实际上,对象引用调用
这种说法更好,因为,当传递的是可变对象时,调用者能发现被调者做出的任何更改(例如插入列表的元素)。