Python 3 入门 - 4. 基本概念与机制

介绍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.
字面值是某些内置类型常量值的表示法。

字面值包括:字符串/字节串字面值、数值字面值(整数字面值、浮点数字面值和虚数字面值)。
字面值是常量,不能被修改。某些操作只能适用于字面值。

下面的例子中,str1str2str3 都是字符串变量,而 "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() 里面定义的变量 spamdo_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 绑定的对象。
  • 在调用函数时,会将实际参数(实参)引入到被调用函数的局部符号表中;因此,实参是使用 按值调用 来传递的(其中的 始终是对象的 引用 而不是对象的值)。实际上,对象引用调用 这种说法更好,因为,当传递的是可变对象时,调用者能发现被调者做出的任何更改(例如插入列表的元素)。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值