1. FlyPython首页
  2. Python高级话题

通过其内置函数了解所有 Python

Python 作为一种语言相对简单。我相信,你可以学到很多关于 Python 及其特性的知识,只需了解它的所有内置函数是什么,以及它们的作用。为了支持这一说法,我将这样做。

需要明确的是,这不会是一篇教程文章。在一篇博文中涵盖如此大量的材料,而从头开始几乎是不可能的。因此,我假设您对 Python 有基本到中级的了解。但除此之外,我们应该很高兴。

指数

那么什么是内建?

Python 中的内置函数是builtins模块中的所有内容。

为了更好地理解这一点,您需要了解LEGB规则。

^ 这定义了在 Python 中查找变量的作用域顺序。它代表:

  • 大号OCAL范围
  • ë nclosing(或外地)范围
  • ģ叶形范围
  • uiltin范围

本地范围

局部作用域是指您所在的当前函数或类附带的作用域。每个函数调用和类实例化都会为您创建一个新的局部作用域,以保存局部变量。

下面是一个例子:

x = 11
print(x)

def some_function():
    x = 22
    print(x)

some_function()

print(x)

运行此代码输出:

11
22
11

所以这就是发生的事情:Doing在它自己的本地命名空间中x = 22定义了一个新变量在那之后,每当函数引用 时,它就意味着它自己范围内的那个。访问外部是指在外部定义的访问some_functionxxsome_function

封闭范围

封闭作用域(或非局部作用域)是指当前函数/类所在的类或函数的作用域。

……我现在已经可以看到你们中的一半人去🤨。那么让我用一个例子来解释:

x = 11
def outer_function():
    x = 22
    y = 789

    def inner_function():
        x = 33
        print('Inner x:', x)
        print('Enclosing y:', y)

    inner_function()
    print('Outer x:', x)

outer_function()
print('Global x:', x)

这个的输出是:

Inner x: 33
Enclosing y: 789
Outer x: 22
Global x: 11

它本质上的意思是,每个新函数/类都会创建自己的本地作用域,与其外部环境分开尝试访问外部变量会起作用,但在局部作用域中创建的任何变量都不会影响外部作用域。这就是为什么将 x 重新定义33为内部函数不会影响x.

但是如果我想影响外部范围呢?

为此,您可以使用nonlocalPython 中关键字来告诉解释器您不是要在本地范围内定义新变量,而是要修改封闭范围内的变量。

def outer_function():
    x = 11

    def inner_function():
        nonlocal x
        x = 22
        print('Inner x:', x)

    inner_function()
    print('Outer x:', x)

这打印:

Inner x: 22
Outer x: 22

全球范围

全局作用域(或模块作用域)只是指定义模块的所有顶级变量、函数和类的作用域。

“模块”是可以运行或导入的任何 python 文件或包。例如。time是一个模块(就像您import time在代码中所做的那样),并且time.sleep()是在time模块的全局范围内定义的函数

Python 中的每个模块都有一些预定义的全局变量,例如__name__and __doc__,它们分别指代模块的名称和模块的文档字符串。你可以在 REPL 中试试这个:

>>> print(__name__)
__main__
>>> print(__doc__)
None
>>> import time
>>> time.__name__
'time'
>>> time.__doc__
'This module provides various functions to manipulate time values.'

内置范围

现在我们进入这个博客的主题——内置作用域。

因此,关于 Python 中的内置作用域有两件事需要了解:

  • 它是定义 Python 的所有顶级函数的范围,例如lenrangeprint
  • 当在局部、封闭或全局作用域中找不到变量时,Python 会在内置函数中查找它。

如果需要,您可以通过导入builtins模块并检查其中的方法来直接检查内置函数

>>> import builtins
>>> builtins.a   # press <Tab> here
builtins.abs(    builtins.all(    builtins.any(    builtins.ascii(

出于某种我不知道的原因,Python__builtins__默认在全局命名空间中公开内置模块因此,您也可以__builtins__直接访问,而无需导入任何内容。请注意,__builtins__可用的是 CPython 实现细节,而其他 Python 实现可能没有它。import builtins是访问内置模块的最正确方法。

所有的内置函数

您可以使用该dir函数打印模块或类中定义的所有变量。所以让我们用它来列出所有的内置函数:

>>> print(dir(__builtins__))
['ArithmeticError', 'AssertionError', 'AttributeError', 'BaseException',
 'BlockingIOError', 'BrokenPipeError', 'BufferError', 'BytesWarning',
 'ChildProcessError', 'ConnectionAbortedError', 'ConnectionError',
 'ConnectionRefusedError', 'ConnectionResetError', 'DeprecationWarning',
 'EOFError', 'Ellipsis', 'EnvironmentError', 'Exception', 'False',
 'FileExistsError', 'FileNotFoundError', 'FloatingPointError',
 'FutureWarning', 'GeneratorExit', 'IOError', 'ImportError',
 'ImportWarning', 'IndentationError', 'IndexError', 'InterruptedError',
 'IsADirectoryError', 'KeyError', 'KeyboardInterrupt', 'LookupError',
 'MemoryError', 'ModuleNotFoundError', 'NameError', 'None',
 'NotADirectoryError', 'NotImplemented', 'NotImplementedError',
 'OSError', 'OverflowError', 'PendingDeprecationWarning',
 'PermissionError', 'ProcessLookupError', 'RecursionError',
 'ReferenceError', 'ResourceWarning', 'RuntimeError', 'RuntimeWarning',
 'StopAsyncIteration', 'StopIteration', 'SyntaxError', 'SyntaxWarning',
 'SystemError', 'SystemExit', 'TabError', 'TimeoutError', 'True',
 'TypeError', 'UnboundLocalError', 'UnicodeDecodeError',
 'UnicodeEncodeError', 'UnicodeError', 'UnicodeTranslateError',
 'UnicodeWarning', 'UserWarning', 'ValueError', 'Warning',
 'ZeroDivisionError', '__build_class__', '__debug__', '__doc__',
 '__import__', '__loader__', '__name__', '__package__', '__spec__',
 'abs', 'all', 'any', 'ascii', 'bin', 'bool', 'breakpoint', 'bytearray',
 'bytes', 'callable', 'chr', 'classmethod', 'compile', 'complex',
 'copyright', 'credits', 'delattr', 'dict', 'dir', 'divmod', 'enumerate',
 'eval', 'exec', 'exit', 'filter', 'float', 'format', 'frozenset',
 'getattr', 'globals', 'hasattr', 'hash', 'help', 'hex', 'id', 'input',
 'int', 'isinstance', 'issubclass', 'iter', 'len', 'license', 'list',
 'locals', 'map', 'max', 'memoryview', 'min', 'next', 'object', 'oct',
 'open', 'ord', 'pow', 'print', 'property', 'quit', 'range', 'repr',
 'reversed', 'round', 'set', 'setattr', 'slice', 'sorted',
 'staticmethod', 'str', 'sum', 'super', 'tuple', 'type', 'vars', 'zip']

……是的,有很多。不过别着急,我们会把它们分成几组,一一击倒。

因此,让我们解决迄今为止最大的一组问题:

例外

Python 有 66 个内置异常类(到目前为止),每个类都旨在供用户、标准库和其他所有人使用,作为解释和捕获代码中错误的有意义的方法。

为了准确解释为什么 Python 中有单独的 Exception 类,这里有一个简单的例子:

def fetch_from_cache(key):
    """Returns a key's value from cached items."""
    if key is None:
        raise ValueError('key must not be None')

    return cached_items[key]

def get_value(key):
    try:
        value = fetch_from_cache(key)
    except KeyError:
        value = fetch_from_api(key)

    return value

专注于get_value功能。如果存在,它应该返回缓存值,否则从 API 获取数据。

在该函数中可能发生 3 件事:

  • 如果key不在缓存中,则尝试访问cached_items[key]会引发KeyError. 这在try块中被捕获,并进行 API 调用以获取数据。
  • 如果他们key存在于缓存中,则返回原样。
  • 还有第三种情况,在哪里keyNone

    如果键是None,则fetch_from_cache引发ValueError,表明提供给该函数的值不合适。并且由于try块只捕获KeyError,因此此错误会直接显示给用户。

    >>> x = None
    >>> get_value(x)
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      File "<stdin>", line 3, in get_value
      File "<stdin>", line 3, in fetch_from_cache
    ValueError: key must not be None
    >>>
    

如果ValueError并且KeyError不是预定义的、有意义的错误类型,就没有任何方法可以以这种方式区分错误类型。

附加:异常琐事

关于异常的一个有趣的事实是,它们可以被子类化以创建您自己的、更具体的错误类型。例如,您可以创建一个InvalidEmailError扩展ValueError, 以在您希望收到电子邮件字符串但它无效时引发错误。如果你这样做,你也可以InvalidEmailError通过这样做来捕捉except ValueError

Another fact about exceptions is that every exception is a subclass of BaseException, and nearly all of them are subclasses of Exception, other than a few that aren’t supposed to be normally caught. So if you ever wanted to be able to catch any exception normally thrown by code, you could do

except Exception: ...

and if you wanted to catch every possible error, you could do

except BaseException: ...

Doing that would even catch KeyboardInterrupt, which would make you unable to close your program by pressing Ctrl+C. To read about the hierarchy of which Error is subclassed from which in Python, you can check the Exception hierarchy in the docs.

现在我应该指出,并非上述输出中的所有大写值都是异常类型,事实上,Python 中还有另一种类型的大写内置对象:常量。所以让我们来谈谈这些。

常数

正好有 5 个常量:True, False, None, Ellipsis, 和NotImplemented

TrueFalse并且None是最明显的常量。

Ellipsis很有趣,它实际上以两种形式表示:单词Ellipsis和符号...它的存在主要是为了支持类型注释,以及一些非常花哨的切片支持。

NotImplemented是所有这些中最有趣的(除了事实上TrueFalse实际功能一样10如果你不知道,但我离题了)NotImplemented在类的运算符定义中使用,当您想告诉 Python 未为此类定义某个运算符时。

现在,我要指出,在Python中的所有对象都添加支持所有的Python运营商,如+-+=,等,通过定义类中的特殊方法,如__add__+__iadd__+=,等等。

让我们看一个简单的例子:

class MyNumber:
    def __add__(self, other):
        return other + 42

这导致我们的对象42在加法过程中充当值

>>> num = MyNumber()
>>> num + 3
45
>>> num + 100
142
附加:右运算符

如果您从上面的代码示例中想知道为什么我没有尝试这样做3 + num,那是因为它还不起作用:

>>> 100 + num
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for +: 'int' and 'MyNumber'

但是,通过添加__radd__运算符可以很容易地添加对它的支持,这增加了对右加法的支持

class MyNumber:
    def __add__(self, other):
        return other + 42

    def __radd__(self, other):
        return other + 42

作为奖励,这还增加了对将两个MyNumber添加在一起的支持

>>> num = MyNumber()
>>> num + 100
142
>>> 3 + num
45
>>> num + num
84

但是假设您只想支持此类的整数加法,而不是浮点数。这是您使用的地方NotImplemented

class MyNumber:
    def __add__(self, other):
        if isinstance(other, float):
            return NotImplemented

        return other + 42

NotImplemented从操作符方法返回告诉 Python 这是一个不受支持的操作。然后 Python 方便地将其包装成一个TypeError带有有意义的消息:

>>> n + 0.12
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for +: 'MyNumber' and 'float'
>>> n + 10
52

关于常量的一个奇怪的事实是它们甚至没有在 Python 中实现,它们直接在 C 代码中实现,例如这样

时髦的全局变量

还有另外一个组中的古怪值的内置命令的输出,我们看到上面:像值__spec____loader____debug__等。

这些实际上并不是builtins模块独有的这些属性都存在于 Python 中每个模块的全局范围内,因为它们是模块属性这些包含有关导入机器所需的模块的信息让我们来看看它们:

__name__

包含模块的名称。例如,builtins.__name__将是字符串'builtins'当你运行一个 Python 文件时,它也作为一个模块运行,它的模块名称是__main__. 这应该解释if __name__ == '__main__'在 Python 文件中使用时的工作原理。

__doc__

包含模块的文档字符串。这就是您执行时显示为模块描述的内容help(module_name)

>>> import time
>>> print(time.__doc__)
This module provides various functions to manipulate time values.

There are two standard representations of time.  One is the number...
>>> help(time)
Help on built-in module time:

NAME
    time - This module provides various functions to manipulate time values.

DESCRIPTION
    There are two standard representations of time.  One is the number...

更多 Python 琐事:这就是为什么PEP8 样式指南说“文档字符串的行长度应该为 72 个字符”:因为文档字符串可以在help()消息中最多缩进两级,因此要整齐地适应 80 个字符宽的终端,它们必须是最多 72 个字符宽。

__package__

此模块所属的包。对于顶级模块,它与__name__. 对于子模块,它是包的__name__. 例如:

>>> import urllib.request
>>> urllib.__package__
'urllib'
>>> urllib.request.__name__
'urllib.request'
>>> urllib.request.__package__
'urllib'

__spec__

这是指模块规范。它包含元数据,例如模块名称、模块类型以及它是如何创建和加载的。

$ tree mytest
mytest
└── a
    └── b.py

1 directory, 1 file

$ python -q
>>> import mytest.a.b
>>> mytest.__spec__
ModuleSpec(name='mytest', loader=<_frozen_importlib_external._NamespaceLoader object at 0x7f28c767e5e0>, submodule_search_locations=_NamespacePath(['/tmp/mytest']))
>>> mytest.a.b.__spec__
ModuleSpec(name='mytest.a.b', loader=<_frozen_importlib_external.SourceFileLoader object at 0x7f28c767e430>, origin='/tmp/mytest/a/b.py')

您可以通过它看到,mytest使用NamespaceLoader从目录中调用东西定位/tmp/mytest,并mytest.a.b使用SourceFileLoader源文件中,加载b.py

__loader__

让我们直接在 REPL 中看看这是什么:

>>> __loader__
<class '_frozen_importlib.BuiltinImporter'>

__loader__被设定为导入机械加载模块时使用的加载器对象。这个特定的_frozen_importlib模块模块中定义,用于导入内置模块。

稍微仔细观察之前的示例,您可能会注意到loader模块规范属性Loader是来自稍微不同的_frozen_importlib_external模块的类。

所以你可能会问,这些奇怪的_frozen模块是什么?好吧,我的朋友,正如他们所说的那样——它们是冻结的模块

这两个模块实际源代码实际上在importlib.machinery模块内部这些_frozen别名是这些加载器源代码的冻结版本。为了创建一个冻结模块,Python 代码被编译成一个代码对象,编组到一个文件中,然后添加到 Python 可执行文件中。

如果您不知道这意味着什么,请不要担心,我们稍后会详细介绍。

Python 冻结了这两个模块,因为它们实现了导入系统的核心,因此在解释器启动时不能像其他 Python 文件一样导入。本质上,它们需要存在以引导导入系统

有趣的是,Python 中还有另一个定义明确的冻结模块:它是__hello__

>>> import __hello__
Hello world!

这是任何语言中最短的 hello world 代码吗?😛

好吧,这个__hello__模块最初是作为对冻结模块的测试添加到 Python 中的,以查看它们是否正常工作。从那以后,它一直作为复活节彩蛋留在该语言中。

__import__

__import__ 是定义 Python 中 import 语句如何工作的内置函数。

>>> import random
>>> random
<module 'random' from '/usr/lib/python3.9/random.py'>
>>> __import__('random')
<module 'random' from '/usr/lib/python3.9/random.py'>
>>> np = __import__('numpy')  # Same as doing 'import numpy as np'
>>> np
<module 'numpy' from '/home/tushar/.local/lib/python3.9/site-packages/numpy/__init__.py'>

本质上,每个导入语句都可以转换为__import__函数调用。在内部,这几乎是 Python 对 import 语句所做的(但直接在 C 中)。

现在,还剩下三个这样的属性:__debug__and__build_class__仅存在于全局而不是模块变量,和__cached__,仅存在于导入的模块中。

__debug__

这是 Python 中的全局常量值,几乎总是设置为True.

它所指的是在调试模式下运行的 Python 默认情况下,Python 始终以调试模式运行。

Python 可以运行的另一种模式是“优化模式”要以“优化模式”运行 python,您可以通过传递-O标志来调用它它所做的只是阻止断言语句做任何事情(至少到目前为止),老实说,这根本没有用。

$ python
>>> __debug__
True
>>> assert False, 'some error'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AssertionError: some error
>>>

$ python -O
>>> __debug__
False
>>> assert False, 'some error'
>>> # Didn't raise any error.

此外,__debug__TrueFalse并且None是唯一真正的常量在Python,即这4个是在Python唯一的全局变量,你不能覆盖新值。

>>> True = 42
  File "<stdin>", line 1
    True = 42
    ^
SyntaxError: cannot assign to True
>>> __debug__ = False
  File "<stdin>", line 1
SyntaxError: cannot assign to __debug__

__build_class__

这个全局变量是在 Python 3.1 中添加的,以允许类定义接受任意位置和关键字参数。为什么这是一个特性有很长的技术原因,它涉及元类等高级主题,所以不幸的是,我不会解释它为什么存在。

但是你需要知道的是,这就是允许你在创建类时做这样的事情的原因:

>>> class C:
...     def __init_subclass__(self, **kwargs):
...         print(f'Subclass got data: {kwargs}')
...
>>> class D(C, num=42, data='xyz'):
...     pass
...
Subclass got data: {'num': 42, 'data': 'xyz'}
>>>

在 Python 3.1 之前,类创建语法只允许传递要继承的基类和元类属性。新要求是允许可变数量的位置和关键字参数。添加到语言中会有点混乱和复杂。

但是,当然,我们在调用常规函数的代码中已经有了这个。因此,建议Class X(...)语法将简单地委托给下面的函数调用:__build_class__('X', ...).

__cached__

这是一个有趣的。

导入模块时,该__cached__属性存储该模块的已编译 Python 字节码的缓存文件的路径

“什么?!”,你可能会说,“Python?编译?”

是的。Python编译。事实上,所有 Python 代码都被编译,但不是机器码——字节码让我通过解释 Python 如何运行您的代码来解释这一点。

以下是 Python 解释器运行您的代码所采取的步骤:

  • 它获取您的源文件,并将其解析为语法树。语法树是您的代码的表示,可以更容易地被程序理解。它会发现并报告代码语法中的任何错误,并确保没有歧义。
  • 下一步是将这个语法树编译成字节码字节码是Python 虚拟机的一组微指令这个“虚拟机”是 Python 解释器逻辑所在的地方。它本质上是在您的机器模拟一个非常简单的基于堆栈的计算机,以便执行您编写的 Python 代码。
  • 然后,您的代码的这种字节码形式将在 Python VM 上运行。字节码指令很简单,比如从当前堆栈中推入和弹出数据。这些指令中的每一个,当一个接一个地运行时,都会执行整个程序。

在下一节中,我们将采用上述步骤的一个非常详细的示例。坚持住!

现在,由于导入模块时上述“编译为字节码”步骤需要花费大量时间,因此 Python 将字节码存储(编组)到一个.pyc文件中,并将其存储在名为__pycache__. __cached__然后导入模块参数指向这个.pyc文件。

当稍后再次导入同一个模块时,Python 会检查.pyc该模块版本是否存在,然后直接导入已编译的版本,从而节省了大量时间和计算。

如果您想知道:是的,您可以直接.pyc在 Python 代码中运行或导入文件,就像.py文件一样:

>>> import test
>>> test.__cached__
'/usr/lib/python3.9/test/__pycache__/__init__.cpython-39.pyc'
>>> exit()

$ cp '/usr/lib/python3.9/test/__pycache__/__init__.cpython-39.pyc' cached_test.pyc
$ python
>>> import cached_test  # Runs!
>>>

所有内置函数,一一

现在我们终于可以使用内置函数了。而且,在上一节的基础上,让我们从一些最有趣的部分开始,这些部分构建了 Python 作为语言的基础。

compile,execeval: 代码如何工作

在上一节中,我们看到了运行一些 Python 代码所需的 3 个步骤。本节将详细介绍这 3 个步骤,以及如何准确观察 Python 正在做什么。

我们以这段代码为例:

x = [1, 2]
print(x)

您可以将此代码保存到文件中并运行它,或者在 Python REPL 中键入它。在这两种情况下,您都会得到[1, 2].

或者,您可以将程序作为字符串提供给 Python 的内置函数exec

>>> code = '''
... x = [1, 2]
... print(x)
... '''
>>> exec(code)
[1, 2]

exec(execute 的缩写)将一些 Python 代码作为字符串接收,并将其作为 Python 代码运行。默认情况下,exec它将在与其余代码相同的范围内运行,这意味着它可以像 Python 文件中的任何其他代码段一样读取和操作变量。

>>> x = 5
>>> exec('print(x)')
5

exec允许您在运行时运行真正的动态代码。例如,您可以在运行时从 Internet 下载 Python 文件,将其内容传递给exec它,它会为您运行它。(但是请永远不要那样做。)

在大多数情况下,您exec在编写代码时并不真正需要它对于实现一些真正动态的行为(例如在运行时创建一个动态类,就像collections.namedtuple这样)或修改从 Python 文件中读取的代码(例如在zxpy 中很有用

但是,这不是今天讨论的主要话题。我们必须了解所有这些花哨的运行时事物是如何exec工作的。

exec不仅可以接收一个字符串并将其作为代码运行,它还可以接收一个代码对象

如前所述,代码对象是 Python 程序的“字节码”版本。它们不仅包含从您的 Python 代码生成的确切指令,而且还存储了该段代码中使用的变量和常量等内容。

代码对象是从 AST(抽象语法树)生成的,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=[
        Name(id='x', ctx=Store())],
      value=List(
        elts=[
          Constant(value=1),
          Constant(value=2)],
        ctx=Load())),
    Expr(
      value=Call(
        func=Name(id='print', ctx=Load()),
        args=[
          Name(id='x', ctx=Load())],
        keywords=[]))],
  type_ignores=[])

乍一看似乎有点过分,但让我分解一下。

AST被作为一个python模块(同样如在此情况下一个Python文件)。

>>> print(ast.dump(tree, indent=2))
Module(
  body=[
    ...

模块的主体有两个孩子(两个语句):

  • 第一个是Assign声明……
    Assign(
        ...
    

    其中分配给目标x……

        targets=[
          Name(id='x', ctx=Store())],
        ...
    

    list具有 2 个常量1的 a 的值2

        value=List(
          elts=[
            Constant(value=1),
            Constant(value=2)],
          ctx=Load())),
      ),
    
  • 第二个是一个Expr语句,在这种情况下是一个函数调用……
      Expr(
        value=Call(
          ...
    

    有名print,有值x

        func=Name(id='print', ctx=Load()),
          args=[
            Name(id='x', ctx=Load())],
    

所以Assign部分是描述,x = [1, 2]部分Expr是描述print(x)现在好像没那么糟吧?

附加功能:Tokenizer

实际上在将代码解析为 AST 之前会发生一个步骤:Lexing

这是指根据 Python 的语法将源代码转换为标记您可以查看 Python 如何标记您的文件,您可以使用该tokenize模块:

$ cat code.py
x = [1, 2]
print(x)

$ py -m tokenize code.py
0,0-0,0:            ENCODING       'utf-8'
1,0-1,1:            NAME           'x'
1,2-1,3:            OP             '='
1,4-1,5:            OP             '['
1,5-1,6:            NUMBER         '1'
1,6-1,7:            OP             ','
1,8-1,9:            NUMBER         '2'
1,9-1,10:           OP             ']'
1,10-1,11:          NEWLINE        '
'
2,0-2,5:            NAME           'print'
2,5-2,6:            OP             '('
2,6-2,7:            NAME           'x'
2,7-2,8:            OP             ')'
2,8-2,9:            NEWLINE        '
'
3,0-3,0:            ENDMARKER      ''

它已经将我们的文件转换成它的裸标记,比如变量名、括号、字符串和数字。例如,它还跟踪每个标记的行号和位置,这有助于指出错误消息的确切位置。

这个“令牌流”就是解析成 AST 的东西。

所以现在我们有了一个 AST 对象。我们可以使用内置函数将其编译为代码对象compile运行exec该代码对象会然后运行它像以前一样:

>>> import ast
>>> code = '''
... x = [1, 2]
... print(x)
... '''
>>> tree = ast.parse(code)
>>> code_obj = compile(tree, 'myfile.py', 'exec')
>>> exec(code_obj)
[1, 2]

但是现在,我们可以看看代码对象的样子。让我们检查它的一些属性:

>>> code_obj.co_code
b'd\x00d\x01g\x02Z\x00e\x01e\x00\x83\x01\x01\x00d\x02S\x00'
>>> code_obj.co_filename
'myfile.py'
>>> code_obj.co_names
('x', 'print')
>>> code_obj.co_consts
(1, 2, None)

您可以看到代码中使用的变量xprint,以及常量12,以及代码对象中提供了更多关于我们的代码文件的信息。这具有直接在 Python 虚拟机中运行所需的所有信息,以便生成该输出。

如果您想深入了解字节码的含义,dis模块下面的附加部分将涵盖这一点。

附加功能:“dis”模块

disPython 中模块可用于以人类可理解的方式可视化代码对象的内容,以帮助弄清楚 Python 在幕后做了什么。它接收字节码、常量和变量信息,并生成以下内容:

>>> import dis
>>> dis.dis('''
... x = [1, 2]
... print(x)
... ''')
  1           0 LOAD_CONST               0 (1)
              2 LOAD_CONST               1 (2)
              4 BUILD_LIST               2
              6 STORE_NAME               0 (x)

  2           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
>>>

它表明:

  • Line 1 creates 4 bytecodes, to load 2 constants 1 and 2 onto the stack, build a list from the top 2 values on the stack, and store it into the variable x.
  • Line 2 creates 6 bytecodes, it loads print and x onto the stack, and calls the function on the stack with the 1 argument on top of it (Meaning, it calls print with argument x). Then it gets rid of the return value from the call by doing POP_TOP because we didn’t use or store the return value from print(x). The two lines at the end returns None from the end of the file’s execution, which does nothing.

当存储为“操作码”(LOAD_CONST例如,您看到的名称是操作码)时,这些字节码中的每一个都是 2 个字节长,这就是操作码左侧的数字彼此相距 2 的原因。它还表明整个代码有 20 个字节长。事实上,如果你这样做:

>>> code_obj = compile('''
... x = [1, 2]
... print(x)
... ''', 'test', 'exec')
>>> code_obj.co_code
b'd\x00d\x01g\x02Z\x00e\x01e\x00\x83\x01\x01\x00d\x02S\x00'
>>> len(code_obj.co_code)
20

您可以确认生成的字节码正好是 20 个字节。

eval非常类似于exec,除了它只接受表达式(不是语句或一组语句,如exec),并且与 不同的是exec,它返回一个值——所述表达式的结果。

下面是一个例子:

>>> result = eval('1 + 1')
>>> result
2

您还可以使用 进行详细的详细路线eval,您只需要说明ast.parse并且compile您希望评估此代码的值,而不是像运行 Python 文件一样运行它。

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

globalslocals:存储所有东西的地方

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

关于语言的工作原理有几个原因,但最明显的原因可以很简单地看出:

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 '_frozen_importlib.BuiltinImporter'>, '__spec__': None,
'__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>,
'value': 5, 'double': <function double at 0x7f971d292af0>}

看看最后一行:不仅value存储在 locals 字典中,函数double本身也存储在那里!这就是 Python 存储数据的方式。

globals非常相似,除了globals总是指向模块范围(也称为全局范围)。所以用这样的代码:

magic_number = 42

def function():
    x = 10
    y = 20
    print(locals())
    print(globals())

locals将只包含xand y,而globals将包含magic_numberandfunction本身。

inputprint:面包和黄油

input并且print可能是您了解 Python 的前两个功能。而且它们看起来很简单,不是吗?input输入一行文本,然后print打印出来,就这么简单。对?

好了,input而且print有可能比你知道什么更多的功能。

这是 的完整方法签名print

print(*values, sep=' ', end='\n', file=sys.stdout, flush=False)

*values只是意味着你可以提供任意数量的位置参数print,并能正确打印出来,分离由默认的空间。

如果您希望分隔符不同,例如。如果您希望每个项目都打印在不同的行上,您可以相应地设置sep关键字,例如'\n'

>>> print(1, 2, 3, 4)
1 2 3 4
>>> print(1, 2, 3, 4, sep='\n')
1
2
3
4
>>> print(1, 2, 3, 4, sep='\n\n')
1

2

3

4
>>>

还有一个end参数,如果你想要一个不同的行尾字符,比如,如果你不想在每次打印结束时打印一个新行,你可以使用end=''

>>> for i in range(10):
...     print(i)
0
1
2
3
4
5
6
7
8
9
>>> for i in range(10):
...     print(i, end='')
0123456789

现在还有两个参数print:fileflush

file指的是您要打印到的“文件”。默认情况下,它指向sys.stdout,这是一个特殊的“文件”包装器,打印到控制台。但是如果你想print写入一个文件,你所要做的就是改变file参数。就像是:

with open('myfile.txt', 'w') as f:
    print('Hello!', file=f)
附加:使用上下文管理器制作印刷作家

Some languages have special objects that let you call print method on them, to write to a file by using the familiar “print” interface. In Python, you can go a step beyond that: you can temporarily configure the print function to write to a file by default!

This is done by re-assigning sys.stdout. If we swap out the file that sys.stdout is assigned to, then all print statements magically start printing to that file instead. How cool is that?

Let’s see with an example:

>>> import sys
>>> print('a regular print statement')
a regular print statement
>>> file = open('myfile.txt', 'w')
>>> sys.stdout = file
>>> print('this will write to the file')  # Gets written to myfile.txt
>>> file.close()

But, there’s a problem here. We can’t go back to printing to console this way. And even if we store the original stdout, it would be pretty easy to mess up the state of the sys module by accident.

For example:

>>> import sys
>>> print('a regular print statement')
a regular print statement
>>> file = open('myfile.txt', 'w')
>>> sys.stdout = file
>>> file.close()
>>> print('this will write to the file')
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
ValueError: I/O operation on closed file.

为避免意外使sys模块处于损坏状态,我们可以使用上下文管理器,以确保sys.stdout在完成后恢复。

import sys
from contextlib import contextmanager

@contextmanager
def print_writer(file_path):
    original_stdout = sys.stdout

    with open(file_path, 'w') as f:
        sys.stdout = f
        yield  # this is where everything inside the `with` statement happens
        sys.stdout = original_stdout

就是这样!以下是您将如何使用它:

with print_writer('myfile.txt'):
    print('Printing straight to the file!')
    for i in range(5):
        print(i)

print('and regular print still works!')

将打印发送到文件或 IO 对象是一个足够常见的用例,它contextlib有一个预定义的函数,称为redirect_stdout

from contextlib import redirect_stdout

with open('this.txt', 'w') as file:
    with redirect_stdout(file):
        import this

with open('this.txt') as file:
    print(file.read())

# Output:
# The Zen of Python, by Tim Peters
# ...

flushprint函数的布尔标志它所做的只是告诉print将文本立即写入控制台/文件,而不是将其放入缓冲区。这通常没有太大区别,但如果您将非常大的字符串打印到控制台,您可能需要设置它True
以避免向用户显示输出时出现延迟。

现在我相信你们中的许多人都对input函数隐藏的秘密感兴趣,但没有。input只需接收一个字符串以显示为提示。是的,无赖,我知道。

str, bytes, int, bool,floatcomplex: 五个原语

Python 正好有 6 种原始数据类型(嗯,实际上只有 5 种,但我们会讲到)。其中 4 个本质上是数字,另外 2 个是基于文本的。让我们先谈谈基于文本的,因为那会简单得多。

str是 Python 中最熟悉的数据类型之一。使用该input方法获取用户输入会为您提供一个字符串,Python 中的所有其他数据类型都可以转换为字符串。这是必要的,因为所有计算机输入/输出都是文本形式,无论是用户 I/O 还是文件 I/O,这可能就是字符串无处不在的原因。

bytes另一方面,实际上是计算中所有 I/O 的基础。如果您了解计算机,您可能会知道所有数据都以位和字节的形式存储和处理——这也是终端真正工作的方式。

如果您想查看inputprint调用下面的字节:您需要查看sys模块中的 I/O 缓冲区sys.stdout.buffersys.stdin.buffer

>>> import sys
>>> print('Hello!')
Hello!
>>> 'Hello!\n'.encode()  # Produces bytes
b'Hello!\n'
>>> char_count = sys.stdout.buffer.write('Hello!\n'.encode())
Hello!
>>> char_count  # write() returns the number of bytes written to console
7

缓冲区对象接收bytes,将它们直接写入输出缓冲区,并返回返回的字节数。

为了证明一切都只是下面的字节,让我们看另一个使用字节打印表情符号的示例:

>>> import sys
>>> '🐍'.encode()
b'\xf0\x9f\x90\x8d'   # utf-8 encoded string of the snake emoji
>>> _ = sys.stdout.buffer.write(b'\xf0\x9f\x90\x8d')
🐍

int是另一种广泛使用的基本原始数据类型。它也是 2 种其他数据类型的最小公分母: ,floatcomplexcomplex是 的超类型float,而后者又是 的超类型int

这意味着所有ints 和 afloat一样有效complex,但反之则不然。同样,所有floats 也作为 a 有效complex

如果您不知道,这complex是 Python 中“复数”的实现。它们是数学中非常常见的工具。

让我们来看看它们:

>>> x = 5
>>> y = 5.0
>>> z = 5.0+0.0j
>>> type(x), type(y), type(z)
(<class 'int'>, <class 'float'>, <class 'complex'>)
>>> x == y == z  # All the same value
True
>>> y
5.0
>>> float(x)    # float(x) produces the same result as y
5.0
>>> z
(5+0j)
>>> complex(x)  # complex(x) produces the same result as z
(5+0j)

现在,我暂时提到 Python 中实际上只有 5 种原始数据类型,而不是 6 种。那是因为,bool实际上不是原始数据类型——它实际上是int!

您可以通过查看mro这些类属性自行检查。

mro代表“方法解析顺序”。它定义了在类上调用的方法的查找顺序。本质上,首先在类本身中查找方法调用,如果它不存在,则在其父类中搜索,然后在其父类中搜索,一直到顶部:objectPython 中的一切都继承自object. 是的,Python 中的几乎所有东西都是对象。

看一看:

>>> int.mro()
[<class 'int'>, <class 'object'>]
>>> float.mro()
[<class 'float'>, <class 'object'>]
>>> complex.mro()
[<class 'complex'>, <class 'object'>]
>>> str.mro()
[<class 'str'>, <class 'object'>]
>>> bool.mro()
[<class 'bool'>, <class 'int'>, <class 'object'>]  # Look!

您可以从它们的“祖先”中看出,所有其他数据类型都不是任何东西的“子类”(除了object,它始终存在)。除了bool,它继承自int.

现在,您可能想知道“为什么?为什么要bool子类int?” 答案有点反气候。这主要是因为兼容性原因。从历史上看,Python 中的逻辑真/假操作仅用于0假和1真。在 Python 2.2 版中,布尔值TrueFalse被添加到 Python 中,它们只是这些整数值的包装器。迄今为止,事实一直如此。就这样。

但是,这也意味着,bool无论好坏,您都可以int预期任何地方传递 a

>>> import json
>>> data = {'a': 1, 'b': {'c': 2}}
>>> print(json.dumps(data))
{"a": 1, "b": {"c": 2}}
>>> print(json.dumps(data, indent=4))
{
    "a": 1,
    "b": {
        "c": 2
    }
}
>>> print(json.dumps(data, indent=True))
{
 "a": 1,
 "b": {
  "c": 2
 }
}

indent=True这里被视为indent=1,所以它有效,但我很确定没有人会打算这意味着缩进 1 个空格。好。

object: 基地

object是整个类层次结构的基类。每个人都继承自object.

object类定义了一些在Python对象的最根本的属性。诸如能够通过 散列对象hash()、能够设置属性并获取它们的值、能够将对象转换为字符串表示等功能。

它通过其预定义的“魔术方法”完成所有这些:

>>> dir(object)
['__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__']

访问属性并obj.x调用__getattr__下面方法。同样,设置新属性和删除属性分别调用__setattr____delattr__对象的hash由预定义的__hash__方法生成,对象的字符串表示来自__repr__.

>>> object()  # This creates an object with no properties
<object object at 0x7f47aecaf210>  # defined in __repr__()
>>> class dummy(object):
...     pass
>>> x = dummy()
>>> x
<__main__.dummy object at 0x7f47aec510a0>  # functionality inherited from object
>>> hash(object())
8746615746334
>>> hash(x)
8746615722250
>>> x.__hash__()  # is the same as hash(x)
8746615722250

关于 Python 中的魔法方法,实际上还有很多要说的,因为它们构成了 Python 面向对象、鸭子类型本质的支柱。但是,这是另一个博客的故事。

有兴趣的可以继续关注哦😉

type: 类工厂

如果object是所有对象的父亲,type则是所有“类”的父亲。就像,虽然所有对象都继承自object,但所有类都继承自type

type是可用于动态创建新类的内置函数。嗯,它实际上有两个用途:

  • 如果给定一个参数,它将返回该参数的“类型”,即用于创建该对象的类:
    >>> x = 5
    >>> type(x)
    <class 'int'>
    >>> type(x) is int
    True
    >>> type(x)(42.0)  # Same as int(42.0)
    42
    
  • 如果给定三个参数,它会创建一个新类。这三个参数是namebasesdict
    • name 定义类的名称
    • bases 定义基类,即超类
    • dict 定义所有类的属性和方法。

    所以这个类定义:

    class MyClass(MySuperClass):
        def x(self):
            print('x')
    

    与此类定义相同:

    def x_function(self):
        print('x')
    
    MyClass = type('MyClass', (MySuperClass), {'x': x_function})
    

    这可以是实现collections.namedtuple类的一种方式,例如,它接受一个类名和一个属性元组。

hashid:平等基础

内置函数hashid构成 Python 中对象相等性的支柱。

默认情况下,Python 对象不可比较,除非它们完全相同。如果您尝试创建两个object()项目并检查它们是否相等……

>>> x = object()
>>> y = object()
>>> x == x
True
>>> y == y
True
>>> x == y  # Comparing two objects
False

结果永远是False这源于objects通过身份比较自己的事实:他们只与自己平等,没有别的。

补充资料:哨兵

出于这个原因,object实例有时也被称为“哨兵”,因为它们可用于准确检查无法复制的值。

哨兵值的一个很好的用例是在您需要为每个可能的值都是有效输入的函数提供默认值的情况下。一个非常愚蠢的例子是这种行为:

>>> what_was_passed(42)
You passed a 42.
>>> what_was_passed('abc')
You passed a 'abc'.
>>> what_was_passed()
Nothing was passed.

乍一看,能够写出这段代码非常简单:

def what_was_passed(value=None):
    if value is None:
        print('Nothing was passed.')
    else:
        print(f'You passed a {value!r}.')

但是,这行不通。那这个呢:

>>> what_was_passed(None)
Nothing was passed.

哦哦。我们不能显式地将 a 传递None给函数,因为这是默认值。我们不能真正使用任何其他文字甚至...省略号,因为那时它们将无法通过。

这是哨兵进来的地方:

__my_sentinel = object()

def what_was_passed(value=__my_sentinel):
    if value is __my_sentinel:
        print('Nothing was passed.')
    else:
        print(f'You passed a {value!r}.')

现在,这将适用于传递给它的每个可能的值。

>>> what_was_passed(42)
You passed a 42.
>>> what_was_passed('abc')
You passed a 'abc'.
>>> what_was_passed(None)
You passed a None.
>>> what_was_passed(object())
You passed a <object object at 0x7fdf02f3f220>.
>>> what_was_passed()
Nothing was passed.

要理解为什么对象只与自身进行比较,我们必须了解is关键字。

Python 的is运算符用于检查两个值是否引用了内存中的相同对象。想想 Python 对象,比如在空间中漂浮的盒子,变量、数组索引等被命名为指向这些对象的箭头。

让我们举一个简单的例子:

>>> x = object()
>>> y = object()
>>> z = y
>>> x is y
False
>>> y is z
True

在上面的代码中,有两个独立的对象和三个标签xyz指向这两个对象:x指向第一个,y并且z都指向另一个。

>>> del x

这将删除箭头x对象本身不受赋值或删除的影响,只有箭头受。但是现在没有箭头指向第一个对象,让它活着是没有意义的。所以Python的“垃圾收集器”摆脱了它。现在我们只剩下一个object.

>>> y = 5

现在y箭头已更改为指向整数对象5z仍然指向第二个object,所以它仍然活着。

>>> z = y * 2

现在 z 指向另一个新对象10,它存储在内存中的某处。现在第二个object也没有任何指向它,因此随后被垃圾收集。

为了能够验证所有这些,我们可以使用id内置函数。id拼出对象在内存中的确切位置,用数字表示。

>>> x = object()
>>> y = object()
>>> z = y
>>> id(x)
139737240793600
>>> id(y)
139737240793616
>>> id(z)
139737240793616  # Notice the numbers!
>>> x is y
False
>>> id(x) == id(y)
False
>>> y is z
True
>>> id(y) == id(z)
True

同一个对象,同一个id不同的对象,不同的id就那么简单。

objects, ==and 的is行为方式相同:

>>> x = object()
>>> y = object()
>>> z = y
>>> x is y
False
>>> x == y
False
>>> y is z
True
>>> y == z
True

这是因为objectfor 的行为==被定义为比较id. 像这样的东西:

class object:
    def __eq__(self, other):
        return self is other

的实际实现object是用 C 编写的。

与 不同==,没有办法覆盖is运算符的行为

另一方面,如果容器类型可以相互替换,则它们是相等的。好的例子是在相同索引处具有相同项目的列表,或者包含完全相同值的集合。

>>> x = [1, 2, 3]
>>> y = [1, 2, 3]
>>> x is y
False       # Different objects,
>>> x == y
True        # Yet, equal.

这些可以这样定义:

class list:
    def __eq__(self, other):
        if len(self) != len(other):
            return False

        return all(x == y for x, y in zip(self, other))

        # Can also be written as:
        return all(self[i] == other[i] for i in range(len(self)))

我们没有看过all或者zip还没有,但所有这样做是确保所有的定列表指数是相等的。

同样,集合是无序的,所以即使它们的位置也不重要,只有它们的“存在”:

class list:
    def __eq__(self, other):
        if len(self) != len(other):
            return False

        return all(item in other for item in self)

现在,关于“等价”的思想,Python 有了hashes的思想任何一条数据的“散列”是指一个预先计算的值,它看起来非常随机,但它可以用来识别那条数据(在某种程度上)。

哈希有两个特定的属性:

  • 同一条数据将始终具有相同的哈希值。
  • 即使非常轻微地更改数据,也会以完全不同的哈希值返回。

这意味着,如果两个值具有相同的哈希值,则它们很可能*也具有相同的值。

比较哈希值是一种非常快速的检查“存在”的方法。这就是字典和集合用来几乎立即在其中查找值的方法:

>>> import timeit
>>> timeit.timeit('999 in l', setup='l = list(range(1000))')
12.224023487000522   # 12 seconds to run a million times
>>> timeit.timeit('999 in s', setup='s = set(range(1000))')
0.06099735599855194  # 0.06 seconds for the same thing

请注意,集合解决方案的运行速度比列表解决方案快数百倍!这是因为它们使用散列值作为“索引”的替代,并且如果相同散列的值已经存储在集合/字典中,Python 可以快速检查它是否是相同的项目。这个过程使得检查存在几乎是即时的。

附加:散列事实

关于散列的另一个鲜为人知的事实是,在 Python 中,所有比较相等的数值都具有相同的散列:

>>> hash(42) == hash(42.0) == hash(42+0j)
True

另一个事实是不可变的容器对象,例如字符串(字符串是字符串的序列)、元组和frozensets,通过组合它们的项目的散列来生成它们的散列。这允许您通过组合函数为您的类创建自定义哈希hash函数:

class Car:
    def __init__(self, color, wheels=4):
        self.color = color
        self.wheels = wheels

    def __hash__(self):
        return hash((self.color, self.wheels))

dirvars:一切都是字典

你有没有想过 Python 如何存储对象、它们的变量、它们的方法等等?我们知道所有对象都有自己附加的属性和方法,但是 Python 究竟是如何跟踪它们的呢?

简单的答案是所有内容都存储在字典中。vars方法公开存储在对象和类中的变量。

>>> class C:
...     some_constant = 42
...     def __init__(self, x, y):
...         self.x = x
...         self.y = y
...     def some_method(self):
...         pass
...
>>> c = C(x=3, y=5)
>>> vars(c)
{'x': 3, 'y': 5}
>>> vars(C)
mappingproxy(
  {'__module__': '__main__', 'some_constant': 42,
  '__init__': <function C.__init__ at 0x7fd27fc66d30>,
  'some_method': <function C.some_method at 0x7fd27f350ca0>,
  '__dict__': <attribute '__dict__' of 'C' objects>,
  '__weakref__': <attribute '__weakref__' of 'C' objects>,
  '__doc__': None
})

如您所见,与对象相关的属性xy相关内容c存储在其自己的字典中,而方法(some_function__init__)实际上作为函数存储在类的字典中。这是有道理的,因为函数本身的代码不会因每个对象而改变,只有传递给它的变量会改变。

这可以通过与c.method(x)以下相同的事实来证明C.method(c, x)

>>> class C:
...     def function(self, x):
...         print(f'self={self}, x={x}')

>>> c = C()
>>> C.function(c, 5)
self=<__main__.C object at 0x7f90762461f0>, x=5
>>> c.function(5)
self=<__main__.C object at 0x7f90762461f0>, x=5

它表明在类中定义的函数实际上只是一个函数,self只是一个作为第一个参数传递的对象。对象语法c.method(x)只是一种更简洁的书写方式C.method(c, x)

现在这里有一个稍微不同的问题。如果vars显示一个类中的所有方法,那么为什么会这样呢?

>>> class C:
...     def function(self, x): pass
...
>>> vars(C)
mappingproxy({
  '__module__': '__main__',
  'function': <function C.function at 0x7f607ddedb80>,
  '__dict__': <attribute '__dict__' of 'C' objects>,
  '__weakref__': <attribute '__weakref__' of 'C' objects>,
  '__doc__': None
})
>>> c = C()
>>> vars(c)
{}
>>> c.__class__
<class '__main__.C'>

🤔__class__c没有在 dict 中定义,也没有在C… 中定义,那么它是从哪里来的呢?

如果您想要确定可以在对象上访问哪些属性的答案,您可以使用dir

>>> dir(c)
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__',
'__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__',
'__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__',
'__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__',
'__subclasshook__', '__weakref__', 'function']

那么其余的属性来自哪里?嗯,故事稍微复杂一些,原因很简单:Python 支持继承。

默认情况下,python 中的所有对象都从object继承,实际上,__class__定义在object

>>> '__class__' in vars(object)
True
>>> vars(object).keys()
dict_keys(['__repr__', '__hash__', '__str__', '__getattribute__', '__setattr__',
'__delattr__', '__lt__', '__le__', '__eq__', '__ne__', '__gt__', '__ge__',
'__init__', '__new__', '__reduce_ex__', '__reduce__', '__subclasshook__',
'__init_subclass__', '__format__', '__sizeof__', '__dir__', '__class__', '__doc__'])

这确实涵盖了我们在dir(c).

既然我已经提到了继承,我想我还应该详细说明“方法解析顺序”是如何工作的。简称 MRO,这是对象从其继承属性和方法的类的列表。这是一个快速示例:

>>> class A:
...     def __init__(self):
...         self.x = 'x'
...         self.y = 'y'
...
>>> class B(A):
...     def __init__(self):
...         self.z = 'z'
...
>>> a = A()
>>> b = B()
>>> B.mro()
[<class '__main__.B'>, <class '__main__.A'>, <class 'object'>]
>>> dir(b)
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__',
'__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__',
'__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__',
'__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__',
'__subclasshook__', '__weakref__', 'x', 'y', 'z']
>>> set(dir(b)) - set(dir(a))  # all values in dir(b) that are not in dir(a)
{'z'}
>>> vars(b).keys()
dict_keys(['z'])
>>> set(dir(a)) - set(dir(object))
{'x', 'y'}
>>> vars(a).keys()
dict_keys(['x', 'y'])

因此,每一级继承都会将较新的方法添加到dir列表中,并dir在子类上显示按其方法解析顺序找到的所有方法。这就是 Python 在 REPL 中建议方法完成的方式:

>>> class A:
...     x = 'x'
...
>>> class B(A):
...     y = 'y'
...
>>> b = B()
>>> b.    # Press <tab> twice here
b.x  b.y  # autocompletion!
附加功能:插槽?

__slots__ 很有趣。

这是 Python 的一种奇怪/有趣的行为:

>>> x = object()
>>> x.foo = 5
AttributeError: 'object' object has no attribute 'foo'
>>> class C:
...     pass
...
>>> c = C()
>>> c.foo = 5
>>> # works?

因此,出于某种原因,您不能将任意变量分配给object,但您可以分配给您自己创建的类的对象。为什么会这样?是特定的object吗?

>>> x = list()
>>> x.foo = 5
AttributeError: 'list' object has no attribute 'foo'

不。发生什么了?

好了,这就是插槽进来首先,让我通过复制出的行为。listobject在我自己的类:

>>> class C:
...     __slots__ = ()
...
>>> c = C()
>>> c.foo = 5
AttributeError: 'C' object has no attribute 'foo'

现在这里有很长的解释:

Python actually has two ways of storing data inside objects: as a dictionary (like most cases), and as a “struct”. Structs are a C language data type, which can essentially be thought of as tuples from Python. Dictionaries use more memory, because they can be expanded as much as you like and rely on extra space for their reliability in quickly accessing data, that’s just how dictionaries are. Structs on the other hand, have a fixed size, and cannot be expanded, but they take the least amount of memory possible as they pack those values one after the other without any wasted space.

这两种在 Python 中存储数据的方式体现在两个对象属性__dict____slots__. 通常,所有实例属性 ( self.foo) 都存储在__dict__字典中,除非您定义了__slots__属性,在这种情况下,对象只能具有恒定数量的预定义属性。

我可以理解这是否太令人困惑。让我举个例子:

>>> class NormalClass:
...     classvar = 'foo'
...     def __init__(self):
...         self.x = 1
...         self.y = 2
...
>>> n = NormalClass()
>>> n.__dict__
{'x': 1, 'y': 2}  # Note that `classvar` variable isn't here.
>>>               # That is stored in `NormalClass.__dict__`
>>> class SlottedClass:
...     __slots__ = ('x', 'y')
...     classvar = 'foo'  # This is fine.
...     def __init__(self):
...         self.x = 1
...         self.y = 2
...         # Trying to create `self.z` here will cause the same
...         # `AttributeError` as before.
...
>>> n = SlottedClass()
>>> s.__dict__
AttributeError: 'SlottedClass' object has no attribute '__dict__'
>>> s.__slots__
('x', 'y')

所以创建插槽可以防止 a__dict__存在,这意味着没有字典可以添加新属性,也意味着节省了内存。基本上就是这样。

AnthonyWritesCode制作了一段视频,内容涉及另一段与插槽及其晦涩行为相关的有趣代码,请务必查看!

hasattr, getattr,setattrdelattr: 属性助手

现在我们已经看到对象与下面的字典几乎相同,让我们在它们之间绘制更多的平行线。

我们知道访问和重新分配字典中的属性是使用索引完成的:

>>> dictionary = {'property': 42}
>>> dictionary['property']
42

而在一个对象上,它是通过.操作符完成的

>>> class C:
...     prop = 42
...
>>> C.prop
42

您甚至可以设置和删除对象的属性:

>>> C.prop = 84
>>> C.prop
84
>>> del C.prop
AttributeError: type object 'C' has no attribute 'prop'

但是字典要灵活得多:例如,您可以检查字典中是否存在属性:

>>> d = {}
>>> 'prop' in d
False
>>> d['prop'] = 'exists'
>>> 'prop' in d
True

可以使用 try-catch 在对象中执行此操作:

>>> class X:
...    pass
...
>>> x = X()
>>> try:
...     print(x.prop)
>>> except AttributeError:
...     print("prop doesn't exist.")
prop doesn't exist.

但是执行此操作的首选方法是直接等效的:hasattr.

>>> class X:
...    pass
...
>>> x = X()
>>> hasattr(x, 'prop')
False
>>> x.prop = 'exists'
>>> hasattr(x, 'prop')
True

字典可以做的另一件事是使用变量来索引字典。你真的不能用对象来做到这一点,对吧?咱们试试吧:

>>> class X:
...     value = 42
...
>>> x = X()
>>> attr_name = 'value'
>>> x.attr_name
AttributeError: 'X' object has no attribute 'attr_name'

是的,它不接受变量的值。这应该是很明显的。但要真正做到这一点,您可以使用getattr, 它接受一个字符串,就像字典键一样:

>>> class X:
...     value = 42
...
>>> x = X()
>>> getattr(x, 'value')
42
>>> attr_name = 'value'
>>> getattr(x, attr_name)
42  # It works!

setattrdelattr以相同的方式工作:它们将属性名称作为字符串,并相应地设置/删除相应的属性。

>>> class X:
...     value = 42
...
>>> x = X()
>>> setattr(x, 'value', 84)
>>> x.value
84
>>> delattr(x, 'value')  # deletes the attribute completety
>>> hasattr(x, 'value')
False  # `value` no longer exists on the object.

让我们尝试使用以下功能之一构建一些有意义的东西:

有时您需要创建一个必须重载的函数,以直接取值或取“工厂”对象,例如,它可以是一个对象或一个函数,它可以按需生成所需的值。让我们尝试实现该模式:

class api:
    """A dummy API."""
    def send(item):
        print(f'Uploaded {item!r}!')

def upload_data(item):
    """Uploads the provided value to our database."""
    if hasattr(item, 'get_value'):
        data = item.get_value()
        api.send(data)
    else:
        api.send(item)

upload_data函数通过检查它是否具有get_value方法来检查我们是否获得了工厂对象如果是,则该函数用于获取要上传的实际值。让我们尝试使用它!

>>> import json
>>> class DataCollector:
...     def __init__(self):
...         self.items = []
...     def add_item(self, item):
...         self.items.append(item)
...     def get_value(self):
...         return json.dumps(self.items)
...
>>> upload_data('some text')
Uploaded 'some text'!
>>> collector = DataCollector()
>>> collector.add_item(42)
>>> collector.add_item(1000)
>>> upload_data(collector)
Uploaded '[42, 1000]'!

super: 传承的力量

super 例如,是 Python 引用超类的方式,以使用其方法。

以这个例子为例,一个类封装了两个项目求和的逻辑:

class Sum:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def perform(self):
        return self.x + self.y

使用这个类非常简单:

>>> s = Sum(2, 3)
>>> s.perform()
5

现在假设您要Sum创建子类以创建一个DoubleSum类,该类具有相同的perform接口,但它返回两倍的值。你会使用super

class DoubleSum(Sum):
    def perform(self):
        parent_sum = super().perform()
        return 2 * parent_sum

我们不需要定义任何已经定义的东西:我们不需要定义__init__也不需要重新编写求和逻辑。我们只是捎带在超类之上。

>>> d = DoubleSum(3, 5)
>>> d.perform()
16

现在还有一些其他方法可以使用该super对象,甚至在类之外:

>>> super(int)
<super: <class 'int'>, NULL>
>>> super(int, int)
<super: <class 'int'>, <int object>>
>>> super(int, bool)
<super: <class 'int'>, <bool object>>

但老实说,我不明白这些会被用来做什么。如果你知道,请在评论中告诉我✨

property,classmethodstaticmethod: 方法装饰器

我们即将完成所有与类和对象相关的内置函数,最后一个是这三个装饰器。

  • property

    @property是当您要为对象中的属性定义 getter 和 setter 时使用的装饰器。在尝试读取或修改对象的属性时,getter 和 setter 提供了一种添加验证或运行一些额外代码的方法。

    这是通过将属性转换为一组函数来完成的:一个函数在您尝试访问该属性时运行,另一个在您尝试更改其值时运行。

    让我们看一个例子,我们尝试确保学生的“marks”属性始终设置为正数,因为分数不能为负:

    class Student:
        def __init__(self):
            self._marks = 0
    
        @property
        def marks(self):
            return self._marks
    
        @marks.setter
        def marks(self, new_value):
            # Doing validation
            if new_value < 0:
                raise ValueError('marks cannot be negative')
    
            # before actually setting the value.
            self._marks = new_value
    

    运行此代码:

    >>> student = Student()
    >>> student.marks
    0
    >>> student.marks = 85
    >>> student.marks
    85
    >>> student.marks = -10
    ValueError: marks cannot be negative
    
  • classmethod

    @classmethod可以在方法上使用以使其成为类方法:这样它就可以获得对类对象的引用,而不是实例 ( self)。

    一个简单的例子是创建一个返回类名的函数:

    >>> class C:
    ...     @classmethod
    ...     def class_name(cls):
    ...         return cls.__name__
    ...
    >>> x = C()
    >>> x.class_name
    'C'
    
  • staticmethod:
    @staticmethod用于将方法转换为静态方法:相当于位于类中的函数,独立于任何类或对象属性。使用 this 完全摆脱了self传递给方法的第一个参数。

    我们可以做一个做一些数据验证的例子:

    class API:
        @staticmethod
        def is_valid_title(title_text):
            """Checks whether the string can be used as a blog title."""
            return title_text.istitle() and len(title_text) < 60
    

这些内置函数是使用一个非常高级的名为descriptors 的主题创建的老实说,描述符是一个非常先进的主题,试图在这里涵盖它除了已经被告知的内容之外没有任何用处。我计划在未来某个时候写一篇关于描述符及其用途的详细文章,敬请期待!

list, tuple, dict,setfrozenset: 容器

Python 中的“容器”是指可以在其中容纳任意数量项目的数据结构。

Python 有 5 种基本的容器类型:

  • list:有序的索引容器。每个元素都存在于特定的索引处。列表是可变的,即可以随时添加或删除项目。
    >>> my_list = [10, 20, 30]  # Creates a list with 3 items
    >>> my_list[0]              # Indexes start with zero
    10
    >>> my_list[1]              # Indexes increase one by one
    20
    >>> my_list.append(40)      # Mutable: can add values
    >>> my_list
    [10, 20, 30, 40]
    >>> my_list[0] = 50         # Can also reassign indexes
    >>> my_list
    [50, 20, 30, 40]
    
  • tuple:就像列表一样排序和索引,但有一个关键区别:它们是不可变的,这意味着一旦创建元组就不能添加或删除项目。
    >>> some_tuple = (1, 2, 3)
    >>> some_tuple[0]              # Indexable
    1
    >>> some_tuple.append(4)       # But NOT mutable
    AttributeError: ...
    >>> some_tuple[0] = 5          # Cannot reassign an index as well
    TypeError: ...
    
  • dict:无序的键值对。键用于访问值。只有一个值可以对应一个给定的键。
    >>> flower_colors = {'roses': 'red', 'violets': 'blue'}
    >>> flower_colors['violets']               # Use keys to access value
    'blue'
    >>> flower_colors['violets'] = 'purple'    # Mutable
    >>> flower_colors
    {'roses': 'red', 'violets': 'purple'}
    >>> flower_colors['daffodil'] = 'yellow'   # Can also add new values
    >>> flower_colors
    {'roses': 'red', 'violets': 'purple', 'daffodil': 'yellow'}
    
  • set: 无序、唯一的数据集合。集合中的项目仅表示它们的存在或不存在。例如,您可以使用集合来查找森林中的树木种类。它们的顺序无关紧要,重要的是它们的存在。
    >>> forest = ['cedar', 'bamboo', 'cedar', 'cedar', 'cedar', 'oak', 'bamboo']
    >>> tree_types = set(forest)
    >>> tree_types
    {'bamboo', 'oak', 'cedar'}      # Only unique items
    >>> 'oak' in tree_types
    True
    >>> tree_types.remove('oak')    # Sets are also mutable
    >>> tree_types
    {'bamboo', 'cedar'}
    
  • Afrozenset与集合相同,但就像tuples一样,是不可变的。
    >>> forest = ['cedar', 'bamboo', 'cedar', 'cedar', 'cedar', 'oak', 'bamboo']
    >>> tree_types = frozenset(forest)
    >>> tree_types
    frozenset({'bamboo', 'oak', 'cedar'})
    >>> 'cedar' in tree_types
    True
    >>> tree_types.add('mahogany')           # CANNOT modify
    AttributeError: ...
    

内置函数list,tupledict也可用于创建这些数据结构的空实例:

>>> x = list()
>>> x
[]
>>> y = dict()
>>> y
{}

但短形式{...}[...]更具有可读性和应该是首选。使用短格式语法也稍微快一点,因为list,dict等是在内置函数中定义的,在变量范围内查找这些名称需要一些时间,而[]被理解为没有任何查找的列表。

bytearraymemoryview:更好的字节接口

Abytearraybytes对象的可变等价物,与列表本质上是可变元组的方式非常相似。

bytearray 很有意义,因为:

  • 许多低级交互都与字节和位操作有关,就像 的这种可怕的实现str.upper一样,因此拥有一个可以改变单个字节的字节数组会更有效率。
  • 字节具有固定大小(即…… 1 个字节)。另一方面,由于 unicode 编码标准“utf-8”,字符串字符可以有各种大小:
    >>> x = 'I♥🐍'
    >>> len(x)
    3
    >>> x.encode()
    b'I\xe2\x99\xa5\xf0\x9f\x90\x8d'
    >>> len(x.encode())
    8
    >>> x[2]
    '🐍'
    >>> x[2].encode()
    b'\xf0\x9f\x90\x8d'
    >>> len(x[2].encode())
    4
    

    所以事实证明,三个字符的字符串 ‘I♥🐍’ 实际上有 8 个字节,而蛇表情符号有 4 个字节长。但是,在它的编码版本中,我们可以访问每个单独的字节。因为它是一个字节,所以它的“值”总是在 0 到 255 之间:

    >>> x[2]
    '🐍'
    >>> b = x[2].encode()
    >>> b
    b'\xf0\x9f\x90\x8d'  # 4 bytes
    >>> b[:1]
    b'\xf0'
    >>> b[1:2]
    b'\x9f'
    >>> b[2:3]
    b'\x90'
    >>> b[3:4]
    b'\x8d'
    >>> b[0]  # indexing a bytes object gives an integer
    240
    >>> b[3]
    141
    

那么让我们来看看一些字节/位操作示例:

def alternate_case(string):
    """Turns a string into alternating uppercase and lowercase characters."""
    array = bytearray(string.encode())
    for index, byte in enumerate(array):
        if not ((65 <= byte <= 90) or (97 <= byte <= 126)):
            continue

        if index % 2 == 0:
            array[index] = byte | 32
        else:
            array[index] = byte & ~32

    return array.decode()

>>> alternate_case('Hello WORLD?')
'hElLo wOrLd?'

这不是一个很好的例子,我不打算费心解释它,但它确实有效,而且比bytes为每个角色更改创建一个新对象要高效得多

同时, amemoryview将这个想法更进一步:它几乎就像一个字节数组,但它可以通过引用引用一个对象或一个切片,而不是为自己创建一个新副本。它允许您传递对内存中字节部分的引用,并就地编辑它:

>>> array = bytearray(range(256))
>>> array
bytearray(b'\x00\x01\x02\x03\x04\x05\x06\x07\x08...
>>> len(array)
256
>>> array_slice = array[65:91]  # Bytes 65 to 90 are uppercase english characters
>>> array_slice
bytearray(b'ABCDEFGHIJKLMNOPQRSTUVWXYZ')
>>> view = memoryview(array)[65:91]  # Does the same thing,
>>> view
<memory at 0x7f438cefe040>  # but doesn't generate a new new bytearray by default
>>> bytearray(view)
bytearray(b'ABCDEFGHIJKLMNOPQRSTUVWXYZ')  # It can still be converted, though.
>>> view[0]  # 'A'
65
>>> view[0] += 32  # Turns it lowercase
>>> bytearray(view)
bytearray(b'aBCDEFGHIJKLMNOPQRSTUVWXYZ')  # 'A' is now lowercase.
>>> bytearray(view[10:15])
bytearray(b'KLMNO')
>>> view[10:15] = bytearray(view[10:15]).lower()
>>> bytearray(view)
bytearray(b'aBCDEFGHIJklmnoPQRSTUVWXYZ')  # Modified 'KLMNO' in-place.

bin, hex, oct, ord,chrascii: 基本转换

binhexoct三重用于在Python基地之间的转换。你给他们一个数字,他们会吐出你如何在你的代码中写出这个数字:

>>> bin(42)
'0b101010'
>>> hex(42)
'0x2a'
>>> oct(42)
'0o52'
>>> 0b101010
42
>>> 0x2a
42
>>> 0o52
42

是的,如果您真的愿意,您可以在代码中以 2 进制、8 进制或 16 进制编写数字。最后,它们都与以常规十进制书写的整数完全相同:

>>> type(0x20)
<class 'int'>
>>> type(0b101010)
<class 'int'>
>>> 0o100 == 64
True

但有时使用其他基数是有意义的,例如在写入字节时:

>>> bytes([255, 254])
b'\xff\xfe'              # Not very easy to comprehend
>>> # This can be written as:
>>> bytes([0xff, 0xfe])
b'\xff\xfe'              # An exact one-to-one translation

或者在编写以八进制实现的特定于操作系统的代码时,例如:

import os
>>> os.open('file.txt', os.O_RDWR, mode=384)    # ??? what's 384
>>> # This can be written as:
>>> os.open('file.txt', os.O_RDWR, mode=0o600)  # mode is 600 -> read-write

请注意,bin例如只应该在您想要创建 Python 整数的二进制表示时使用:如果您想要一个二进制字符串,最好使用 Python 的字符串格式:

>>> f'{42:b}'
101010

ordchr用于转换 ascii 以及 unicode 字符及其字符代码:

>>> ord('x')
120
>>> chr(120)
'x'
>>> ord('🐍')
128013
>>> hex(ord('🐍'))
'0x1f40d'
>>> chr(0x1f40d)
'🐍'
>>> '\U0001f40d'  # The same value, as a unicode escape inside a string
'🐍'

这很简单。

format:简单的文本转换

format(string, spec)只是另一种方式string.format(spec)

Python 的字符串格式化可以做很多有趣的事情,比如:

>>> format(42, 'c')             # int to ascii
'*'
>>> format(604, 'f')            # int to float
'604.000000'
>>> format(357/18, '.2f')       # specify decimal precision
'19.83'
>>> format(604, 'x')            # int to hex
'25c'
>>> format(604, 'b')            # int to binary
'1001011100'
>>> format(604, '0>16b')        # binary with zero-padding
'0000001001011100'
>>> format('Python!', '🐍^15')  # centered aligned text
'🐍🐍🐍🐍Python!🐍🐍🐍🐍'

我在这里有一整篇关于字符串格式的文章,所以请查看更多内容。

anyall

这两个是我最喜欢的一些内置函数。不是因为它们非常有用或强大,而只是因为它们是Pythonic某些逻辑部分可以使用anyor重写all,这将立即使其更短且更具可读性,这就是 Python 的全部意义所在。下面是一个这样的例子:

假设您有一堆来自 API 的 JSON 响应,并且您想确保所有这些响应都包含一个 ID 字段,该字段正好是 20 个字符长。你可以这样写你的代码:

def validate_responses(responses):
    for response in responses:
        # Make sure that `id` exists
        if 'id' not in response:
            return False
        # Make sure it is a string
        if not isinstance(response['id'], str):
            return False
        # Make sure it is 20 characters
        if len(response['id']) != 20:
            return False

    # If everything was True so far for every
    # response, then we can return True.
    return True

或者,我们可以这样写:

def validate_responses(responses):
    return all(
        'id' in response
        and isinstance(response['id'], str)
        and len(response['id']) == 20
        for response in responses
    )

什么all做的是它发生在布尔值的迭代器,并返回False如果遇到甚至一个单一的False迭代器值。否则返回True

我喜欢使用 的方式all,因为它读起来和英语完全一样:“如果 id 存在,则返回整数,并且在所有响应中都是 20 的长度。”

这是另一个示例:尝试查看列表中是否有任何回文:

def contains_palindrome(words):
    for word in words:
        if word == ''.join(reversed(word)):
            return True

    # Found no palindromes in the end
    return False

对比

def contains_palindrome(words):
    return any(word == ''.join(reversed(word)) for word in words)

并且我认为措辞应该是显而易见的,这any与所有内容相反:True即使有一个值True,它也返回,否则返回False

附加:任何/所有中的列表压缩

请注意,使用anyor的代码all也可以写成列表理解:

>>> any([num == 0 for num in nums])

而不是生成器表达式:

>>> any(num == 0 for num in nums)

请注意[]第二个中缺少方括号。在这种情况下,你应该总是更喜欢使用生成器表达式,因为生成器在 Python 中是如何工作的。

Generators are constructs that generate new values lazily. What this means is that instead of computing and storing all the values inside a list, it generates one value, provides it to the program, and only generates the next value when it is required.

This means that there’s a huge difference between these two lines of code:

>>> any(num == 10 for num in range(100_000_000))
True
>>> any([num == 10 for num in range(100_000_000)])
True

第二个不仅在运行之前无缘无故地将 1 亿个值存储在列表中all,而且在我的机器上也需要超过 10 秒。同时,因为第一个是一个生成器表达式,它一个一个地生成从 0 到 10 的数字,将它们交给any,一旦计数达到 10,any就会中断迭代并True几乎立即返回这也意味着,在这种情况下,它的运行速度实际上快了 1000 万倍。

是的。永远不要在内部传递列表推导式,any或者all当您可以传递生成器时。

abs, divmod,powround: 数学基础

这四个数学函数在编程中非常常见,以至于它们被直接扔到了始终可用的内置函数中,而不是放在math模块中。

它们非常简单:

  • abs 返回数字的绝对值,例如:
    >>> abs(42)
    42
    >>> abs(-3.14)
    3.14
    >>> abs(3-4j)
    5.0
    
  • divmod 除法运算后返回商和余数:
    >>> divmod(7, 2)
    (3, 1)
    >>> quotient, remainder = divmod(5327, 100)
    >>> quotient
    53
    >>> remainder
    27
    
  • pow 返回值的指数(幂):
    >>> pow(100, 3)
    1000000
    >>> pow(2, 10)
    1024
    
  • round 返回一个四舍五入到给定十进制精度的数字:
    >>> import math
    >>> math.pi
    3.141592653589793
    >>> round(math.pi)
    3
    >>> round(math.pi, 4)
    3.1416
    >>> round(1728, -2)
    1700
    

isinstanceissubclass:运行时类型检查

您已经看到了type内置函数,使用这些知识,您可以根据需要实现运行时类型检查,如下所示:

def print_stuff(stuff):
    if type(stuff) is list:
        for item in stuff:
            print(item)
    else:
        print(stuff)

在这里,我们试图检查项目是否是 a list,如果是,我们单独打印其中的每个项目。否则,我们只打印项目。这就是代码的作用:

>>> print_stuff('foo')
foo
>>> print_stuff(123)
123
>>> print_stuff(['spam', 'eggs', 'steak'])
spam
eggs
steak

它确实有效!所以是的,您可以在运行时检查变量的类型并更改代码的行为。但是,上面的代码实际上有很多问题。下面是一个例子:

>>> class MyList(list):
...     pass
...
>>> items = MyList(['spam', 'eggs', 'steak'])
>>> items
['spam', 'eggs', 'steak']
>>> print_stuff(items)
['spam', 'eggs', 'steak']

Welp,items很明显仍然是一个列表,但print_stuff不再识别它了。原因很简单,因为type(items)是 now MyList,不是list

这段代码似乎违反了五项 SOLID 原则之一,称为“Liskov 替换原则”。该原则说“超类的对象应该可以用它的子类的对象替换而不破坏应用程序”。这对于继承成为有用的编程范式很重要。

我们函数的根本问题是它不考虑继承。这正是isinstance它的用途:它不仅检查对象是否是类的实例,还检查该对象是否是子类的实例:

>>> class MyList(list):
...     pass
...
>>> items = ['spam', 'eggs', 'steak']
>>> type(items) is list
True
>>> isinstance(items, list)
True   # Both of these do the same thing
>>> items = MyList(['spam', 'eggs', 'steak'])
>>> type(items) is list
False  # And while `type` doesn't work,
>>> isinstance(items, list)
True   # `isinstance` works with subclasses too.

类似地,issubclass检查一个类是否是另一个类的子类。for 的第一个参数isinstance是一个对象,但issubclass它是另一个类:

>>> issubclass(MyList, list)
True

type检查替换为isinstance,上面的代码将遵循 Liskov 替换原则。但是,它仍然可以改进。以这个为例:

>>> items = ('spam', 'eggs', 'steak')
>>> print_stuff(items)
('spam', 'eggs', 'steak')

显然,除了list现在,它不处理其他容器类型您可以尝试通过检查isinstance列表、元组、字典等来解决此问题。但有多远?您要添加多少对象支持?

对于这种情况,Python 为您提供了一堆“基类”,您可以使用它们来测试类的某些“行为”,而不是测试类本身。在我们的例子中,行为是其他对象的容器,所以基类被恰当地称为Container

>>> from collections.abc import Container
>>> items = ('spam', 'eggs', 'steak')
>>> isinstance(items, tuple)
True
>>> isinstance(items, list)
False
>>> isinstance(items, Container)
True  # This works!

我们应该在这里使用IterableorCollection基类,但是对于字符串,它的行为会有所不同,因为字符串是可迭代的,但不是容器。这就是为什么Container选择在这里。这只是为了便于解释,在实际代码中,建议准确查看哪个基类适合您的用例。您可以使用docs找到

每个容器对象类型都将True在针对Container基类的检查中返回issubclass也有效:

>>> from collections.abc import Container
>>> issubclass(list, Container)
True
>>> issubclass(tuple, Container)
True
>>> issubclass(set, Container)
True
>>> issubclass(dict, Container)
True

所以把它添加到我们的代码中,它变成:

from collections.abc import Container

def print_stuff(stuff):
    if isinstance(stuff, Container):
        for item in stuff:
            print(item)
    else:
        print(stuff)

这种检查类型的方式实际上有一个名字:它被称为“鸭子类型”。

callable 和鸭子打字基础

Famously, Python is referred to as a “duck-typed” language. What it means is that instead of caring about the exact class an object comes from, Python code generally tends to check instead if the object can satisfy certain behaviours that we are looking for.

In the words of Alex Martelli:

“You don’t really care for IS-A — you really only care for BEHAVES-LIKE-A-(in-this-specific-context), so, if you do test, this behaviour is what you should be testing for.

In other words, don’t check whether it IS-a duck: check whether it QUACKS-like-a duck, WALKS-like-a duck, etc, etc, depending on exactly what subset of duck-like behaviour you need to play your language-games with.”

To explain this, I’ll give you a quick example:

Python 中的一些项目可以被“调用”以返回一个值,比如函数和类,而其他项目则不能,TypeError如果你尝试会引发一个

>>> def magic():
...     return 42
...
>>> magic()  # Works fine
42
>>> class MyClass:
...     pass
...
>>> MyClass()  # Also works
<__main__.MyClass object at 0x7f2b7b91f0a0>
>>> x = 42
>>> x()  # Doesn't work
TypeError: 'int' object is not callable

你如何开始检查你是否可以尝试“调用”一个函数、类等等?答案实际上很简单:您只需查看对象是否实现了__call__特殊方法。

>>> def is_callable(item):
...     return hasattr(item, '__call__')
...
>>> is_callable(list)
True
>>> def function():
...     pass
...
>>> is_callable(function)
True
>>> class MyClass:
...     pass
...
>>> is_callable(MyClass)
True
>>> is_callable('abcd')
False

这几乎就是callable内置函数的作用:

>>> callable(list)
True
>>> callable(42)
False

顺便说一句,这些“特殊方法”是 Python 的大部分语法和功能的工作方式:

  • x() 和做一样 x.__call__()
  • items[10] 和做一样 items.__getitem__(10)
  • a + b 和做一样 a.__add__(b)

几乎每个 python 行为都有一个底层的“特殊方法”,或者有时被称为“dunder 方法”的定义在下面。

如果您想详细了解这些 dunder 方法,可以阅读有关Python 数据模型的文档页面

sortedreversed:序列操纵器

排序和反转数据序列可能是任何编程语言中最常用的算法操作。和顶级sortedreversed让你做到这一点。

  • sorted
    此函数对传入的数据进行排序,并返回排序后的
    list类型。

    >>> items = (3, 4, 1, 2)
    >>> sorted(items)
    [1, 2, 3, 4]
    

    它使用由最早的 Python 向导之一 Tim Peters 创建的“TimSort”算法。

    还有两个其他参数sorted可以采用:reverse,当设置为True以相反顺序对数据进行排序时;key,它接受一个用于每个元素的函数,以根据每个项目的自定义属性对数据进行排序。让我们来看看它:

    >>> items = [
    ...   {'value': 3},
    ...   {'value': 1},
    ...   {'value': 2},
    ... ]
    >>> sorted(items, key=lambda d: d['value'])
    [{'value': 1}, {'value': 2}, {'value': 3}]
    >>> names = ['James', 'Kyle', 'Max']
    >>> sorted(names, key=len)  # Sorts by name length
    ['Max', 'Kyle', 'James']
    

    另请注意,虽然list.sort()已经是对列表进行排序的.sort()一种方法,但该方法仅存在于列表中,而sorted可以采用任何可迭代对象。

  • reversed

    reversed是一个函数,它接受任何序列类型并返回一个generator,它以相反的顺序产生值。

    返回生成器很好,因为这意味着反转某些对象根本不需要额外的内存空间,例如rangeor list,其反转值可以一一生成。

    >>> items = [1, 2, 3]
    >>> x = reversed(items)
    >>> x
    <list_reverseiterator object at 0x7f1c3ebe07f0>
    >>> next(x)
    3
    >>> next(x)
    2
    >>> next(x)
    1
    >>> next(x)
    StopIteration # Error: end of generator
    >>> for i in reversed(items):
    ...     print(i)
    ...
    3
    2
    1
    >>> list(reversed(items))
    [3, 2, 1]
    

mapfilter:功能原语

现在在 Python 中,一切都可能是对象,但这并不一定意味着您的 Python 代码需要面向对象。实际上,您可以在 Python 中编写非常易于阅读的函数式代码。

如果您不知道什么是函数式语言或函数式代码,那么想法是所有功能都是通过函数提供的。没有类和对象、继承等的正式概念。从本质上讲,所有程序都只是通过将数据传递给函数并将修改后的值返回给您来操作数据。

这可能过于简单化,这里不要过多讨论我的定义。但我们正在继续前进。

函数式编程中两个非常常见的概念是mapfilter,Python 为它们提供了内置函数:

  • map

    map 是一个“高阶函数”,这只是意味着它是一个接受另一个函数作为参数的函数。

    什么map确实是从一组值到另一个地图。一个非常简单的例子是方形映射:

    >>> def square(x):
    ...     return x * x
    ...
    >>> numbers = [8, 4, 6, 5]
    >>> list(map(square, numbers))
    [64, 16, 36, 25]
    >>> for squared in map(square, numbers):
    ...     print(squared)
    ...
    64
    16
    36
    25
    

    map接受两个参数:一个函数和一个序列。它只是以每个元素作为输入运行该函数,并将所有输出存储在一个新列表中。map(square, numbers)取每个数字并返回一个平方数列表。

    请注意,我必须这样做list(map(square, numbers)),这是因为map它本身返回一个生成器。当您请求它们时,这些值一次一个懒惰地映射,例如,如果您遍历一个映射值,它将在序列的每个项目上一个一个地运行映射函数。这意味着 map 不会存储映射值的完整列表,并且不会在不需要时浪费时间计算额外值。

  • filter

    filter与 非常相似map,但它不会将每个值映射到新值,而是根据条件过滤一系列值

    这意味着过滤器的输出将包含与输入的项目相同的项目,除了一些可能会被丢弃。

    一个非常简单的例子是从结果中过滤掉奇数:

    >>> items = [13, 10, 25, 8]
    >>> evens = list(filter(lambda num: num % 2 == 0, items))
    >>> evens
    [10, 8]
    

    一些人可能已经意识到这些函数本质上与列表推导式做同样的事情,你是对的!

    列表推导式基本上是一种更 Pythonic、更易读的方式来编写这些完全相同的东西:

    >>> def square(x):
    ...     return x * x
    ...
    >>> numbers = [8, 4, 6, 5]
    >>> [square(num) for num in numbers]
    [64, 16, 36, 25]
    
    >>> items = [13, 10, 25, 8]
    >>> evens = [num for num in items if num % 2 == 0]
    >>> evens
    [10, 8]
    

    您可以自由使用似乎更适合您的用例的任何语法。

len, max,minsum: 聚合函数

Python 有一些聚合函数:将一组值组合成单个结果的函数。

我认为只需一个小代码示例就足以解释这四个:

>>> numbers = [30, 10, 20, 40]
>>> len(numbers)
4
>>> max(numbers)
40
>>> min(numbers)
10
>>> sum(numbers)
100

其中三个实际上可以采用任何容器数据类型,如集合、字典甚至字符串:

>>> author = 'guidovanrossum'
>>> len(author)
14
>>> max(author)
'v'
>>> min(author)
'a'

sum需要接受一个数字容器。这意味着,这有效:

>>> sum(b'guidovanrossum')
1542

我会让你弄清楚这里发生了什么;)

iternext:高级迭代

iternext定义 for 循环的工作机制。

一个看起来像这样的 for 循环:

for item in mylist:
    print(item)

实际上在内部做这样的事情:

mylist_iterable = iter(mylist)
while True:
    try:
        item = next(mylist_iterable)

        print(item)

    except StopIteration:
        break

Python 中的 for 循环是一个巧妙伪装的 while 循环。当您对列表或任何其他支持迭代的数据类型进行迭代时,这仅意味着它理解该iter函数,并返回一个“迭代器”对象。

Python 中的迭代器对象做两件事:

  • 每次您将它们传递给它们时,它们都会产生新值 next
  • StopIteration当迭代器用完值时,它们会引发内置异常。

这就是所有 for 循环的工作方式。

顺便说一句,生成器也遵循迭代器协议:

>>> gen = (x**2 for x in range(1, 4))
>>> next(gen)
1
>>> next(gen)
4
>>> next(gen)
9
>>> next(gen)
Error: StopIteration

range, enumerateand zip: 方便迭代

你已经知道了range它最多接受 3 个值,并返回一个可迭代的整数值:

>>> list(range(10))
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> list(range(3, 8))
[3, 4, 5, 6, 7]
>>> list(range(1, 10, 2))
[1, 3, 5, 7, 9]
>>> list(range(10, 1, -2))
[10, 8, 6, 4, 2]

enumeratezip实际上也真的很有用。

enumerate 当您需要访问列表中元素的索引和值时非常有用。

而不是做:

>>> menu = ['eggs', 'spam', 'bacon']
>>> for i in range(len(menu)):
...     print(f'{i+1}: {menu[i]}')
...
1: eggs
2: spam
3: bacon

你可以这样做:

>>> menu = ['eggs', 'spam', 'bacon']
>>> for index, item in enumerate(menu, start=1):
...     print(f'{index}: {item}')
...
1: eggs
2: spam
3: bacon

类似地,zip用于从多个迭代中获取索引值。

而不是做:

>>> students = ['Jared', 'Brock', 'Jack']
>>> marks = [65, 74, 81]
>>> for i in range(len(students)):
...     print(f'{students[i]} got {marks[i]} marks')
...
Jared got 65 marks
Brock got 74 marks
Jack got 81 marks

你可以做:

>>> students = ['Jared', 'Brock', 'Jack']
>>> marks = [65, 74, 81]
>>> for student, mark in zip(students, marks):
...     print(f'{student} got {mark} marks')
...
Jared got 65 marks
Brock got 74 marks
Jack got 81 marks

两者都可以帮助大规模简化迭代代码。

slice

一个slice目标是什么引擎盖下使用时,你尝试切片一个Python迭代。

my_list[1:3]比如,[1:3]是不是特殊的一部分,仅1:3是。方括号仍在尝试索引列表!但是1:3这些方括号内,这里实际上创建了一个slice对象。

这就是为什么,my_list[1:3]实际上相当于my_list[slice(1, 3)]

>>> my_list = [10, 20, 30, 40]
>>> my_list[1:3]
[20, 30]
>>> my_list[slice(1, 3)]
[20, 30]
>>> nums = list(range(10))
>>> nums
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> nums[1::2]
[1, 3, 5, 7, 9]
>>> s = slice(1, None, 2)  # Equivalent to `[1::2]`
>>> s
slice(1, None, 2)
>>> nums[s]
[1, 3, 5, 7, 9]

如果您想更多地了解切片、切片的工作原理以及可以使用切片完成的所有操作,我将在此处单独的文章中介绍

breakpoint: 内置调试

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

是什么pdb让你做的是停止代码的执行在任何时刻,检查变量的值,如果你喜欢运行一些代码,然后你甚至可以做花哨的东西就像在同一时间运行一行代码,或检查状态解释器内的堆栈帧。

使用pdb调试代码,通过在慢慢走,看到这行代码得到执行,并检查对象和变量的值是一个更有效的方法来调试代码比使用print语句。

不幸的是,没有任何好的方法可以在博客中以文本格式显示正在使用的调试器。但是,如果您有兴趣,AnthonyWritesCode 有一个非常好的视频解释了它的一些功能。

open: 文件输入/输出

open 是让您读取和写入文件的函数。

这……其实挺简单的,也没有什么晦涩难懂的地方可以解释,所以我也懒得理会。如果您想了解更多信息,可以阅读有关读写文件官方文档

repr: 开发者方便

repr是一个有趣的。它的预期用例只是帮助开发人员。

repr用于创建对象的有用字符串表示,希望能简明地描述对象及其当前状态。这样做的目的是能够通过查看对象的 repr 来调试简单的问题,而不必在每一步都探查 ints 属性。

这是一个很好的例子:

>>> class Vector:
...     def __init__(self, x, y):
...         self.x = x
...         self.y = y
...
>>> v = Vector(3, 5)
>>> v
<__main__.Vector object at 0x7f27dff5a1f0>

默认值repr根本没有帮助。您必须手动检查其属性:

>>> dir(v)
['__class__', ... , 'x', 'y']
>>> v.x
3
>>> v.y
5

但是,如果你repr对它实施一个友好的

>>> class Vector:
...     def __init__(self, x, y):
...         self.x = x
...         self.y = y
...     def __repr__(self):
...         return f'Vector(x={self.x}, y={self.y})'
>>> v = Vector(3, 5)
>>> v
Vector(x=3, y=5)

现在您不需要想知道这个对象包含什么。就在你面前!

help,exitquit: 站点内置函数

现在,这些内置函数不是真正的内置函数。就像在builtins模块中一样,它们并没有真正定义相反,它们在site模块中定义,然后在site模块运行时注入内置函数

site是一个在启动 Python 时默认自动运行的模块。它负责设置一些有用的东西,包括使 pip 包可用于导入,以及在 REPL 中设置选项卡完成等。

它所做的另一件事是设置以下几个有用的全局函数:

  • help用于查找模块和对象的文档。这相当于调用pydoc.doc().
  • exitquit退出 Python 进程。调用它们等同于调用sys.exit().

这三个文本也由站点模块定义,在 REPL 中输入它们会打印出它们的文本,这license()是一个交互式会话。

下一个是什么?

好吧,这是交易。Python是巨大的。

这里有一些我们甚至还没有触及的事情:

  • 线程/多处理
  • 异步计算
  • 类型注释
  • 元类
  • 弱引用
  • 200 个左右的内置模块可以完成从 html 模板到发送电子邮件到加密的所有工作。

这可能还不是全部。

但是,重要的是您现在对 Python 的基础知识了解很多。你知道是什么让 Python 打勾,你了解它的优势。

剩下的东西你可以随手拿起,你只需要意识到它们的存在!

官方 Python 教程有一个关于内置模块的部分,围绕它们的文档实际上非常好。在您需要时阅读它几乎可以帮助您根据需要找出所有内容。

结束

非常感谢您阅读这篇文章。如果您成功阅读了整篇文章,那么恭喜您!我很想听听你的想法✨

原创文章,作者:flypython,如若转载,请注明出处:http://flypython.com/advanced-python/547.html