在python中的内建函数和辅助工具进行调试

本文涉及的产品
Serverless 应用引擎 SAE,800核*时 1600GiB*时
性能测试 PTS,5000VUM额度
简介: 【6月更文挑战第18天】本文介绍Python的调试,涉及对代码的字节码和语法结构进行深入分析。通过这些工具,开发者能更好地理解和调试代码执行流程。

简介

python代码对象存储了代码的字节码和变量信息,通过如下工具进行代码分析。
ast模块解析为抽象语法树(AST)。
tokenize模块用于词法分析,将源代码转换为标记。
compile函数将AST编译为代码对象,
dis模块则可视化解码这些对象,展示字节码。
pdb是内置调试器,提供断点、单步调试等功能。
globals()locals()存储变量的全局和局部值。
breakpoint()是Python 3.7引入的便捷调试入口。

1 调试:内建函数

代码对象是python程序的字节码版本,不仅包含从您的python代码生成的确切指令还存储了该段代码使用的变量和常量。

代码对象从AST(抽象语法树)生成,它们本身由在代码字符串运行的解析器生成的。

查看抽象语法树,首先使用 ast模块从我们的代码生成一个AST,

    >>> import ast
    >>> code = """x = [1,2];print(x);"""
    >>> tree = ast.parse(code)
    >>> print(ast.dump(tree, indent=2))
    Module(
      body=[
        Assign(   // 声明
          targets=[   // 分配给目标x
            Name(id='x', ctx=Store())],
          value=List(  // list具有2个常数 1 和a的值2
            elts=[
              Constant(value=1),
              Constant(value=2)],
            ctx=Load())),
        Expr(  // Expr语句,Call函数调用
          value=Call(
            func=Name(id='print', ctx=Load()),  // 调用函数print 
            args=[
              Name(id='x', ctx=Load())],   // 被调用函数print的值为 x
            keywords=[]))],
      type_ignores=[])
    >>>

2 内建函数:标记器

在AST被解析出之前发生的一个步骤 Lexing 词法分析
根据python语法将源代码转换为标记,

您可查看python如何标记您的文件,使用tokenize

查看python 如何解析代码为 token, “令牌流”被解析为 AST。

有以下代码

       # paramsclient.py
    import os

    if __name__ == '__main__':
        print(os.path)

python -m tokenize paramsclient.py

    0,0-0,0:            ENCODING       'utf-8'
    1,0-1,6:            NAME           'import'
    1,7-1,9:            NAME           'os'
    1,9-1,11:           NEWLINE        '\r\n'
    2,0-2,2:            NL             '\r\n'
    3,0-3,2:            NL             '\r\n'
    4,0-4,2:            NAME           'if'
    4,3-4,11:           NAME           '__name__'
    4,12-4,14:          OP             '=='
    4,15-4,25:          STRING         "'__main__'"
    4,25-4,26:          OP             ':'
    4,26-4,28:          NEWLINE        '\r\n'
    5,0-5,4:            INDENT         '    '
    5,4-5,9:            NAME           'print'
    5,9-5,10:           OP             '('
    5,10-5,12:          NAME           'os'
    5,12-5,13:          OP             '.'
    5,13-5,17:          NAME           'path'
    5,17-5,18:          OP             ')'
    5,18-5,19:          NEWLINE        ''
    6,0-6,0:            DEDENT         ''
    6,0-6,0:            ENDMARKER      ''

tokenize将我们的文件转为它的裸标记,比如变量名,括号字符串,数组。

它还跟踪每个标记的行号和位置,例如,这有助于指出错误小心的确切位置。
这个“令牌流”被解析为 AST,解析AST树后 使用 内置函数将其编译为代码对象。

compile 在代码对象上运行exec,像以前一样运行它

    >>> code_Obj = compile(tree, "mf.py", 'exec')
>>> exec(code_Obj)

code_obj 有一些属性

>>> dir(code_Obj)

            ['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__ne_
    _', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'co_argcount', 'co_cellvars', 'co_code', 'co_consts', 'co_filename',
     'co_firstlineno', 'co_flags', 'co_freevars', 'co_kwonlyargcount', 'co_lnotab', 'co_name', 'co_names', 'co_nlocals', 'co_posonlyargcount', 'co_stacksize', 'co_varnames', 'replace']


>>> code_Obj.co_filename
    'mf.py'

>>> code_Obj.co_names
    ('x', 'print')   // 变量 x 和 print

>>> code_Obj.co_consts
    (1, 2, None)

这些具有直接在python虚拟机中运行所需的所有信息,以便生成该输出。

3 dis 与 pdb

dis模块可用于以人类可理解的方式可视化代码对象内容
帮助了解python幕后所做工作。

它接收字节码,常量,和变量信息

>>> import dis
>>> dis.dis(code)
  1           0 LOAD_CONST               0 (1)
              2 LOAD_CONST               1 (2)
              4 BUILD_LIST               2
              6 STORE_NAME               0 (x)
              8 LOAD_NAME                1 (print)
             10 LOAD_NAME                0 (x)
             12 CALL_FUNCTION            1
             14 POP_TOP
             16 LOAD_CONST               2 (None)
             18 RETURN_VALUE

dis.dis 表示一行,创建 10个字节码,加载print并 x 到 堆栈上,并在堆栈 1上调用带有参数的函数。 然后它通过do摆脱调用返回值。

POP_TOP因为我们没有使用或存储来自 print(x) 的最后两行None从执行的末尾返回,它什么也没有做。

当存储为操作码时,如 POP_TOP名,这些字节码中的每一个都是2个字节长LOAD_CONST,

这就是操作码左侧的数字彼此相距2 的原因,它还表明整个代码有20个字节长。 事实上,使用len可查看

    >>> code_Obj.co_code
    b'd\x00d\x01g\x02Z\x00e\x01e\x00\x83\x01\x01\x00d\x02S\x00'
    >>> len(code_Obj.co_code)
    20

eval与exec非常相似,除了它只接收表达式,不是语句或一组语句,如exec并且与exec不同,它返回一个值,所述表达式的结果

    result = eval("1 + 1")
    result
        2

或者使用详细的路线eval,需要您告诉ast.parse并且compule您期望评估此代码的价值,而不是以python文件运行

>>> expr = ast.parse('1+1', mode='eval')
>>> code_obj = compile(expr, '<code>', 'eval')
>>> eval(code_obj)
2

4 调试时 globals 和 locals 存储所有内容的两个对象

虽然生成的代码对象存储了一段代码中定义的逻辑和常量,但它们不存储一件事是正在使用的变量的实际值。

关于语言工作原理 有几个原因

    def double(number):
        return number * 2

这个函数代码对象将存储常量 2,以及变量名 number,但它显然不能包含实际值的number,

因为在函数实际运行之前不会给它,

python将所有内容存储在与每个本地范围关联的字典中。这意味着每段代码都有自己定义的 “本地范围”

可用locals() 在该代码内部访问,其中包含与每个变量名称对应的值。

实例:

    >>> value = 5
    >>> def double(number):
    ...     return number * 2
    ...
    >>> double(value)
    10
    >>> locals()
    {'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <class '_froz
    en_importlib.BuiltinImporter'>, '__spec__': None, '__annotations__': {}, '__builtins__': <
    module 'builtins' (built-in)>, 'ast': <module 'ast' from '\lib\\ast.py'>, 'co
    de': 'x = [1,2];print(x);', 'tree': <ast.Module object at 0x0000027AF729F8B0>, 'code_Obj':
     <code object <module> at 0x0000027AF72A2920, file "mf.py", line 1>, 'x': [1, 2], 'dis': <
    module 'dis' from '\lib\\dis.py'>, 'expr': <ast.Expression object at 0x000002
    7AF72DC910>, 'code_obj': <code object <module> at 0x0000027AF72F6030, file "<code>", line
    1>, 'value': 5, 'double': <function double at 0x0000027AF72E3310>}
    >>> globals()
    {'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <class '_froz...'}

此时全局与本地是相同的。

    >>> globals() == locals()
    True

globals非常相似,只是globals总是指向模块范围(也称全局范围)

  • 内置调试

    breakpoint是一个被添加到 Python 3.7 中的内置函数,作为进入调试会话的一种更简单的方法。
    本质上,它只是set_trace()从pdb模块调用,该模块是 Python 内置的调试器模块。

让您pdb可以随时停止代码的执行,检查变量的值,如果您愿意,可以运行一些代码,

然后您甚至可以做一些花哨的事情,比如一次运行一行代码,或者检查状态解释器内部的堆栈帧

python /tests/paramsclient.py

> \paramsclient.py(47)alternate_case()
-> array = bytearray(string.encode())  # 把字符添加到一个 byte数组
(Pdb) 
  • 查询断点设置位置
        (Pdb) list .
         42          :param string:
         43          :return:
         44          """
         45      
         46          breakpoint()
         47  ->        array = bytearray(string.encode())  # 把字符添加到一个 byte数组
         48          for index, byte in enumerate(array):
         49              if not (( 65 <= byte <= 90) or (97 <= byte <= 126)):
         50                  continue
         51              if index %2 == 0:
         52                  array[index] = byte | 32
        (Pdb) 
  • 查询附近更多代码

         (Pdb) ll
    
  • 查询执行输入的地方

         (Pdb) where
          \paramsclient.py(68)<module>()
         -> print(alternate_case("Hello,World!"))
         > \paramsclient.py(47)alternate_case()
         > 
    
  • 把字符添加到一个 byte数组

         -> array = bytearray(string.encode())  
         (Pdb) 
    
  • 向上进入调用者帧的层级

         (Pdb) up
         > \paramsclient.py(68)<module>()
         -> print(alternate_case("Hello,World!"))
         (Pdb) up
         *** Oldest frame
         (Pdb) up
         *** Oldest frame
    
  • 向下进入调用者帧的层级

         (Pdb) down
         > \paramsclient.py(47)alternate_case()
    
  • 把字符添加到一个 byte数组

         -> array = bytearray(string.encode())  
         (Pdb) down
         *** Newest frame
    
    • 在pdb调试器中打印

         使用内建函数打印
         (Pdb) locals()
                 {'string': 'Hello,World!'}
         (Pdb) globals()
      

      {'name': 'main', 'doc': None, 'package': None, 'loader': <_frozen_importlib_external.SourceFileLoader object at 0x00000198E3176CD0>, ...}

         (Pdb) 
      
  • 查询环境中加载的对象

         (Pdb) string
                 'Hello,World!'
    
  • p 相当于使用 pring

          (Pdb) p string
                  'Hello,World!'
    
  • pp 相当于使用 ppring

      (Pdb) pp xl
      {'a': 1, 'b': 2, 'c': 3}
    
  • 转跳到将要执行的一行

      n
      (Pdb) n
      >  \paramsclient.py(48)alternate_case()
      -> for index, byte in enumerate(array):
      (Pdb) 
    
  • 单步调试python代码 s

      (Pdb) s
      > 
      > \paramsclient.py(49)alternate_case()
      -> if not (( 65 <= byte <= 90) or (97 <= byte <= 126)):
      (Pdb) s
      > \paramsclient.py(51)alternate_case()
      -> if index %2 == 0:
      (Pdb) s
      > \paramsclient.py(52)alternate_case()
      -> array[index] = byte | 32
      (Pdb) 
    
  • 显示python执行器

     (Pdb) p sys.executable
     '\python.exe'
     (Pdb) 
    
  • r 是return 的简写,将执行到函数的return位置

     (Pdb) r
     --Return--
     > \paramsclient.py(55)alternate_case()->'hElLo,wOrLd!'
     -> return array.decode()
     (Pdb) r
     hElLo,wOrLd!
     --Return--
     > \paramsclient.py(69)<module>()->None
     -> print(alternate_case("Hello,World!"))
     (Pdb) 
    
  • 查看当前位置

          (Pdb) list
           64          #         print(i)
           65          #
           66          # print("regular print still works normal.")
           67          xl = {"a":1, "b":2, "c":3}
           68      
           69  ->        print(alternate_case("Hello,World!"))
           70      
          [EOF]
    
  • 运行到下一个return的位置

          (Pdb) r
          --Call--
          > \lib\logging\__init__.py(2126)shutdown()
          -> def shutdown(handlerList=_handlerList):
    
  • 设置运行时断点 和 条件断点

5 小结

本文解释几个常见的调式方法,并且它们如何与实际使用结合。

目录
相关文章
|
3天前
|
测试技术 开发者 Python
Python中的装饰器:提升函数的灵活性和可重用性
在Python编程中,装饰器是一种强大的工具,它可以在不修改函数本身的情况下,动态地扩展函数的功能。本文将介绍装饰器的工作原理及其在实际开发中的应用,帮助读者更好地理解和利用这一特性。
|
2天前
|
存储 Python
在Python中,匿名函数(lambda表达式)是一种简洁的创建小型、一次性使用的函数的方式。
【6月更文挑战第24天】Python的匿名函数,即lambda表达式,用于创建一次性的小型函数,常作为高阶函数如`map()`, `filter()`, `reduce()`的参数。lambda表达式以`lambda`开头,后跟参数列表,冒号分隔参数和单行表达式体。例如,`lambda x, y: x + y`定义了一个求和函数。在调用时,它们与普通函数相同。例如,`map(lambda x: x ** 2, [1, 2, 3, 4, 5])`会返回一个列表,其中包含原列表元素的平方。
13 4
|
3天前
|
JSON 数据格式 索引
Python内置函数如`print()`输出信息,`len()`计算长度
【6月更文挑战第23天】Python内置函数如`print()`输出信息,`len()`计算长度,`type()`识别类型,`range()`生成序列,`sum()`求和,`min()`和`max()`找极值,`abs()`取绝对值,`round()`四舍五入,`sorted()`排序,`zip()`和`enumerate()`组合及遍历,`map()`和`filter()`应用函数。标准库如`os`用于操作系统交互,`sys`处理解释器信息,`math`提供数学运算,`re`支持正则表达式,`json`处理JSON数据。学习这些能提升编程效率。
18 5
|
2天前
|
Python
在Python中,高阶函数是指那些可以接受一个或多个函数作为参数,并返回一个新的函数的函数。
【6月更文挑战第24天】Python的高阶函数简化代码,增强可读性。示例:`map()`检查用户名合法性,如`[&quot;Alice&quot;, &quot;Bob123&quot;, &quot;Charlie!&quot;, &quot;David7890&quot;]`;`reduce()`与`lambda`结合计算阶乘,如1到10的阶乘为3628800;`filter()`找出1到100中能被3整除的数,如[3, 6, 9, ..., 99]。
12 3
|
3天前
|
分布式计算 大数据 调度
MaxCompute产品使用问题之为什么用python写的udf函数跑起来比本地还要慢
MaxCompute作为一款全面的大数据处理平台,广泛应用于各类大数据分析、数据挖掘、BI及机器学习场景。掌握其核心功能、熟练操作流程、遵循最佳实践,可以帮助用户高效、安全地管理和利用海量数据。以下是一个关于MaxCompute产品使用的合集,涵盖了其核心功能、应用场景、操作流程以及最佳实践等内容。
|
3天前
|
SQL 分布式计算 大数据
MaxCompute产品使用问题之建了一个python 的 UDF脚本,生成函数引用总是说类不存在,是什么导致的
MaxCompute作为一款全面的大数据处理平台,广泛应用于各类大数据分析、数据挖掘、BI及机器学习场景。掌握其核心功能、熟练操作流程、遵循最佳实践,可以帮助用户高效、安全地管理和利用海量数据。以下是一个关于MaxCompute产品使用的合集,涵盖了其核心功能、应用场景、操作流程以及最佳实践等内容。
|
1天前
|
Python
python函数
python函数
5 0
|
1天前
|
Python
python之print函数
python之print函数
7 0
|
2天前
|
Python
使用Python计算有效值函数(RMS值)
使用Python计算有效值函数(RMS值)
8 0