Fluent Python2 【Chapter9_QA】

1.python中None,和NoneType,以及Null之间有何区别?

Python中NoneNoneTypeNull之间有一些区别和联系,具体如下:

  1. None
    • None是Python中表示"无"或"不存在"的一个特殊常量对象。
    • 它属于NoneType这个类型。
    • 在布尔环境中,None等价于False
  2. NoneType
    • NoneType是Python内置的一个数据类型。
    • 它只有一个值,就是None
    • NoneTypeNone所属的类型。
    • None的类型是NoneType,可以用type(None)进行验证。
  3. Null
    • Null是一个大写字母开头的标识符,并不是Python中的关键字。
    • Python中没有Null这个值或对象。
    • Null通常是其他编程语言(如Java、C++等)中表示"无"或"不存在"的关键字或常量。

总的来说:

  • None是Python中表示"无"的特殊常量对象。
  • NoneTypeNone所属的数据类型。
  • Python中没有Null这个关键字或常量,它通常出现在其他编程语言中。

Python使用None来表示"无"或"不存在",而不是使用Null。这样做是为了与Python命名惯例保持一致,避免与其他语言的Null产生混淆。

因此,在Python中,我们使用None来表示空值或缺失值,比如:

x = None  # x 为 None
my_list = [1, 2, None, 4]  # 列表中可以包含 None
result = some_function() or None  # 如果函数返回值为假,则为 None

而在其他语言中,通常使用Null来表示同样的概念。

 

2. Iterable和Iterator之间的关系是什么?

IterableIterator在Python中是两个相关但不同的概念,它们之间的关系如下:

Iterable(可迭代对象):

  • 是实现了__iter__方法的对象,或者说能够返回一个迭代器的对象。
  • 可迭代对象可以使用iter()函数获取一个迭代器。
  • 字符串、列表、元组、字典、集合等都是可迭代对象。
  • 可迭代对象本身不能被迭代,但可以用于创建迭代器。

Iterator(迭代器):

  • 是实现了__iter____next__方法的对象。
  • 迭代器负责迭代可迭代对象,并返回其中的元素。
  • 可以使用next()函数或者在循环中直接迭代一个迭代器。
  • 当没有元素可迭代时,迭代器会引发StopIteration异常。

它们之间的关系可以简单概括为:

  • 可迭代对象用于创建迭代器。
  • 迭代器用于实际迭代可迭代对象中的元素。

例如,对于一个列表[1, 2, 3]来说:

  • 列表本身是一个可迭代对象。
  • iter([1, 2, 3])会创建一个迭代器,用于遍历列表中的元素。
  • 通过next()函数或for循环可以从迭代器中获取元素。
my_list = [1, 2, 3]

# 获取列表的迭代器
list_iter = iter(my_list)

# 使用next()函数迭代
print(next(list_iter))  # 输出 1
print(next(list_iter))  # 输出 2

# 使用for循环迭代
for item in list_iter:
    print(item)  # 输出 3

总之,可迭代对象定义了如何获取迭代器,而迭代器负责实际迭代可迭代对象中的元素。这种设计模式使得Python中的迭代变得更加灵活和统一。

通俗地解释一下可迭代对象(Iterable)和迭代器(Iterator)之间的关系:

可迭代对象(Iterable)就像是一个装有东西的容器,比如一个盒子或者一个袋子。而迭代器(Iterator)就像是一个小帮手,它能够按照一定的顺序,从容器里一个一个地取出东西来。

举个例子,假设有一个装有苹果的盒子,这个盒子就是一个可迭代对象。如果你想从盒子里一个一个地取出苹果来,就需要一个小帮手(迭代器)来帮你做这件事。

这个小帮手会先从盒子里拿出一个苹果,然后你可以问它:"还有苹果吗?"如果有,它就会说:"有",并把下一个苹果拿出来给你。如果没有了,它就会说:"没有了,盒子里的苹果都被取出来了"。

可见,可迭代对象好比一个容器,而迭代器则是一个可以逐个取出容器内东西的小帮手。可迭代对象本身不能被迭代(取出东西),你需要先从可迭代对象那里得到一个迭代器,然后使用这个迭代器来一个一个地取出容器内的东西。

在Python中,列表、字符串、字典等都是可迭代对象。当你使用for循环迭代一个可迭代对象时,Python会自动为你创建一个迭代器,并使用这个迭代器来获取可迭代对象中的元素,完成迭代过程。

这就是可迭代对象和迭代器之间的关系,一个提供容器,一个负责取出容器内的元素。它们的配合使得Python中的迭代变得简单和统一。

 

3. @functools.wraps(func)的概念理解  [from clockdeco.py]

import time
import functools

def clock(func):
    @functools.wraps(func)
    def clocked(*args, **kwargs):
        t0 = time.perf_counter()
        result = func(*args, **kwargs)
        elapsed = time.perf_counter() - t0
        name = func.__name__
        arg_lst = [repr(arg) for arg in args]
        arg_lst.extend(f'{k}={v!r}' for k, v in kwargs.items())
        arg_str = ', '.join(arg_lst)
        print(f'[{elapsed:0.8f}s] {name}({arg_str}) -> {result!r}')
        return result
    return clocked

@functools.wraps(func) 是 Python 中一个非常有用的装饰器, 它主要用于保留被装饰函数的元数据信息,比如函数名、文档字符串(docstring)、参数列表等。下面我将从几个方面详细解释它:

概念functools.wraps 是 Python 内置模块 functools 中的一个函数, 它本身也是一个装饰器。当它应用于另一个函数时,会将被装饰函数的一些元数据信息复制到装饰器函数上。

作用:

当我们使用装饰器来包装一个函数时,被装饰的函数实际上是被一个新的函数取代了。这会导致一些问题,比如查看被装饰函数的 __name__ 属性时显示的是装饰器函数的名称,而不是原函数的名称。

同理,原函数的文档字符串等信息也会丢失。 @functools.wraps(func) 可以帮助我们解决这个问题,保留被装饰函数的元数据信息。

通俗解释:

假设你有一个漂亮的礼物盒,上面写着礼物的名称和描述。但当你把这个礼物盒装进另一个外包装盒时,原来的信息就被新盒子的外观所取代了。

你无法直接看到原有的礼物信息。 这就有点像被装饰的函数被新的装饰器函数所取代后,丢失了自己原有的元数据信息。

@functools.wraps(func) 就相当于在外包装盒上贴上一张标签,标签上写着原礼物盒的所有信息。这样即使被包装了,你还是能获取到原有的礼物信息。

使用举例:

import functools

def decorator(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        # 一些额外的操作
        result = func(*args, **kwargs)
        # 一些额外的操作
        return result
    return wrapper

@decorator
def add(x, y):
    """Add two numbers"""
    return x + y

print(add.__name__)  # 输出 'add' 而不是 'wrapper'
print(add.__doc__)   # 输出函数文档字符串 'Add two numbers'

如果没有使用 @functools.wraps(func),打印 add.__name__ 会输出 'wrapper'add.__doc__ 会输出 None

通过应用 @functools.wraps(func),我们成功保留了 add 函数原有的 __name____doc__ 信息。这对于代码的可读性和调试非常有帮助。

总之,@functools.wraps(func) 可以帮助我们在使用装饰器时,保留被装饰函数原有的元数据信息,使代码更加易读和易于调试。在定义装饰器时,建议一并使用它,以获得更好的代码质量。

 

 4. locals()函数作用的理解。

locals()是一个Python内置函数,它返回一个字典,包含当前局部作用域内的所有变量名及其对应的值。

举个简单的例子: 

c = 3
def
my_function(): a = 1 b = 2 print(locals()) my_function()

输出:

{'a': 1, 'b': 2}

在上面的例子中, locals()返回了一个字典,包含在my_function()函数内定义的两个局部变量ab以及它们的值。

再看一个稍微复杂的例子:

def outer_func(x):
    y = 2
    
    def inner_func(z):
        w = 4
        print(locals())
        
    inner_func(3)

outer_func(1)

输出:

{'z': 3, 'w': 4}

在这个例子中, locals()返回了inner_func()内的局部变量zw。它没有包含外层函数outer_func()中的变量xy,因为locals()只返回当前局部作用域中的变量。

总的来说, locals()提供了一种方便的方式来查看和操作当前函数或代码块中的所有局部变量,非常有用。但是需要注意它只返回局部作用域的变量,不包括全局变量或外层函数的变量。

 

5. 理解下如下参数化装饰器的代码

# clockdeco_param

import time

DEFAULT_FMT = '[{elapsed:0.8f}s] {name}({args}) -> {result}'

def clock(fmt=DEFAULT_FMT):  # <1>
    def decorate(func):      # <2>
        def clocked(*_args): # <3>
            t0 = time.perf_counter()
            _result = func(*_args)  # <4>
            elapsed = time.perf_counter() - t0
            name = func.__name__
            args = ', '.join(repr(arg) for arg in _args)  # <5>
            result = repr(_result)  # <6>
            print(fmt.format(**locals()))  # <7>
            return _result  # <8>
        return clocked  # <9>
    return decorate  # <10>

if __name__ == '__main__':

    @clock()  # <11>
    def snooze(seconds):
        time.sleep(seconds)

    for i in range(3):
        snooze(.123)


# clockdeco_param_demo1

import time
from clockdeco_param import clock

@clock('{name}: {elapsed}s')
def snooze(seconds):
    time.sleep(seconds)

for i in range(3):
    snooze(.123)

问题(1):不太理解,为什么当执行snooze(.123)时,0.123作为位置参数传递给了snooze函数。

在Python中,当您调用一个函数时,可以传递位置参数或关键字参数。位置参数是按照顺序传递的值,而关键字参数则是使用参数名来指定。

在这个案例中,当执行snooze(.123)时,0.123就是作为位置参数传递给snooze函数的。

现在让我们来看一下clock装饰器中的clocked函数的定义:

def clocked(*_args):
    ...

这里的*_args是一个特殊的语法,它允许clocked函数接收任意数量的位置参数,并将它们打包成一个元组。这个元组就被赋值给了变量_args

所以,当执行snooze(.123)时,0.123作为位置参数传递给了snooze函数。由于snooze函数被clock装饰器包装了,所以实际上调用的是clocked函数。

clocked函数中,*_args会接收传递给snooze函数的所有位置参数,在这个案例中就是0.123这一个参数。_args的值,因此变成了元组(0.123,)

这就是为什么在输出中,您会看到_args: (0.123,)*_args语法让clocked函数能够透明地接收传递给snooze函数的任意位置参数,而不需要预先知道参数的数量和值。

总的来说,*_args是一种Python语法糖,它允许函数以元组的形式接收任意数量的位置参数,使得函数更加灵活和通用。在这个装饰器的案例中,它让clocked函数能够获取传递给被装饰函数的所有参数值。

 

问题(2) func是写在decorate函数中作为参数存在的,而clocked是decorate内部的函数, 

为什么当执行snooze(.123)时,0.123作为位置参数传递给了snooze函数??

在这段代码中,clock是一个装饰器函数,它返回另一个函数decoratedecorate函数的作用是接受一个函数func作为参数,并返回一个新的函数clocked

当您使用@clock()语法来装饰snooze函数时,实际上发生了以下过程:

  1. clock()返回decorate函数。
  2. decorate函数接收snooze函数作为参数func
  3. decorate函数内部,它定义了一个新的函数clocked
  4. clocked函数包含了对原始snooze函数的调用,以及其他的功能(如计时等)。
  5. decorate函数最终返回这个clocked函数。

所以,当您执行snooze(.123)时,实际上执行的是clocked函数,而不是原始的snooze函数。这是因为@clock()语法将snooze函数替换为了clocked函数。

clocked函数内部,它使用*_args语法来接收传递给snooze函数的所有位置参数。所以,当您执行snooze(.123)时,0.123作为位置参数传递给了snooze函数,

但由于snooze函数被替换为了clocked函数,所以最终0.123作为位置参数传递给了clocked函数中的*_args

总的来说,装饰器模式的核心思想就是通过函数的嵌套和返回,在不修改原始函数的情况下,为其增加新的功能。在这个案例中,clock装饰器返回了clocked函数,而clocked函数在执行原始函数的同时,还添加了计时和打印输出的功能。

 

 问题(3) 一般装饰器最多写两个嵌套函数,为什么这里写了三个嵌套函数呢?

在这个案例中,使用了三个嵌套函数的原因是为了实现参数化的装饰器。

通常情况下,装饰器确实只需要两个嵌套函数:外层函数(这里是clock)接收参数,内层函数(这里是decorate)接收被装饰的函数作为参数。

但是,如果需要为装饰器提供参数,就需要一个额外的函数层级来处理这种情况。这就是为什么这个示例中有三个嵌套函数。

具体来说:

  1. 最外层的clock函数接收一个可选的fmt参数,用于自定义输出格式。
  2. clock函数返回decorate函数。
  3. decorate函数接收被装饰的函数func作为参数。
  4. decorate函数内部,定义了clocked函数,它是真正装饰func的函数,包含了计时和格式化输出的逻辑。
  5. decorate函数返回clocked函数。

通过这种三层嵌套的结构,实现了以下两个目标:

  1. 允许用户通过调用clock(fmt)来提供自定义的输出格式字符串。
  2. clocked函数作为真正的装饰器函数,对被装饰的函数func进行了增强。

如果不需要为装饰器提供参数,那么可以只使用两个嵌套函数。但是,如果需要为装饰器提供参数,就需要像这个示例一样,使用三个嵌套函数来实现参数化的装饰器。

这种设计模式提供了更大的灵活性,允许在不修改被装饰函数的情况下,通过传递不同的参数来定制装饰器的行为。

 

6. @singledispatch的概念的理解与使用

@singledispatch 是 Python 提供的一种函数重载机制,用于实现泛函数(generic function)。它是 Python 3.4 版本引入的语法糖,用于简化泛函数的实现。

概念和作用:

在许多编程语言中,函数重载是非常普遍的特性。它允许一个函数根据传入参数的不同类型来执行不同的代码逻辑。但在 Python 中,函数重载的实现并不直接,需要一些技巧。@singledispatch 就是为了解决这个问题而引入的。

@singledispatch 修饰一个普通函数,将其转化为泛函数。泛函数可以拥有多个不同的实现版本,针对不同的参数类型执行不同的处理逻辑。通过使用 @singledispatch 修饰的泛函数和 @<泛函数名>.register 修饰的专用函数,开发者可以很容易地实现函数重载。

通俗解释:

我们可以通过一个形象的比喻来解释 @singledispatch 的作用。假设你想开发一个图像处理程序,它可以处理多种不同类型的图像文件,比如 PNG、JPG、BMP 等。

每种图像文件的处理方式都不太一样,因为它们的格式不同。你可以编写一个统一的 process_image() 函数,但在这个函数内部,你需要根据输入图像的类型执行不同的处理逻辑。

这时,@singledispatch 就可以帮助你更优雅地解决这个问题。首先,你用 @singledispatch 修饰一个默认的 process_image() 函数。

然后,你可以使用 @process_image.register 来注册针对每种图像类型的专用函数。这样,当你调用 process_image() 并传入一个图像时,Python 就会自动选择合适的专用函数来处理该图像。

使用举例:

from functools import singledispatch

@singledispatch
def process_data(data):
    print(f"Default processing for data: {data}")

@process_data.register(str)
def _(data):
    print(f"Processing string data: {data}")

@process_data.register(int)
def _(data):
    print(f"Processing integer data: {data}")

@process_data.register(list)
def _(data):
    print(f"Processing list data: {data}")

process_data(10)       # Processing integer data: 10
process_data("hello")  # Processing string data: hello
process_data([1, 2, 3])# Processing list data: [1, 2, 3]
process_data({1: 'a'}) # Default processing for data: {1: 'a'}

在这个例子中,我们定义了一个 process_data 泛函数,它接受任何类型的数据。我们使用 @process_data.register 注册了专门处理 strintlist 类型的函数。

当我们调用 process_data() 时,Python 会自动选择合适的专用函数。如果传入的数据类型没有对应的专用函数,则执行默认的 process_data() 函数。

没有这个概念会带来的问题和不便:

如果没有 @singledispatch 这种机制,要实现类似的函数重载功能就会变得很麻烦。你可能需要编写大量的 if...elif...else 语句来检查参数类型,然后执行对应的处理逻辑。

这不仅代码冗长、可读性差,而且还容易出错,尤其是在处理多个参数的情况下。另外,如果需要添加新的参数类型,你还需要修改现有的代码,这违背了开闭原则。

使用 @singledispatch可以让你的代码更加简洁、易读,同时也具有很好的可扩展性。你只需要使用注册器(@<泛函数名>.register)为新的类型注册对应的处理函数即可,而不需要修改原有代码。这种解耦的设计也提高了代码的可维护性。

总的来说,@singledispatch 为 Python 带来了函数重载这一常见的编程语言特性,提高了语言的表现力,使开发者能够以更优雅、更 Pythonic 的方式来编写泛型代码。

 

7. 这个limit_denomiator()函数的意思


from fractions import Fraction

def limit_denominator(self, max_denominator=1000000):
        """Closest Fraction to self with denominator at most max_denominator.

        >>> Fraction('3.141592653589793').limit_denominator(10)
        Fraction(22, 7)
        >>> Fraction('3.141592653589793').limit_denominator(100)
        Fraction(311, 99)
        >>> Fraction(4321, 8765).limit_denominator(10000)
        Fraction(4321, 8765)

        """

这个函数的作用是将一个分数转换为最接近的分母不超过给定值的分数。具体来说:

  • 参数max_denominator用于指定最大的分母值,默认为1000000。
  • 函数会返回一个与原始分数最接近但分母不超过max_denominator的分数。

通俗易懂的解释就是,这个函数能够将一个分数转换为另一个最接近的分数,这个转换后的分数的分母不超过指定的最大分母值。

例如,如果给定一个分数3.141592653589793,调用limit_denominator(10)后,它将返回最接近的分母不超过10的分数,即22/7

如果调用limit_denominator(100),则返回的分数是311/99

 

8. python中html.escape()函数的含义

如下是escape的源码,这个源码一看基本就秒懂了。

def escape(s, quote=True):
    """
    Replace special characters "&", "<" and ">" to HTML-safe sequences.
    If the optional flag quote is true (the default), the quotation mark
    characters, both double quote (") and single quote (') characters are also
    translated.
    """
    s = s.replace("&", "&amp;") # Must be done first!
    s = s.replace("<", "&lt;")
    s = s.replace(">", "&gt;")
    if quote:
        s = s.replace('"', "&quot;")
        s = s.replace('\'', "&#x27;")
    return s

html.escape()是Python中用于转义HTML字符的一个函数,它可以将一些特殊字符转换为对应的HTML实体字符编码,以防止浏览器将其解析为HTML标签或其他内容。

通俗解释: 假设你有一些文本需要显示在网页上,其中包含一些特殊字符如 <>&等。如果你直接将这些文本插入HTML代码中,浏览器可能会将这些特殊字符解析为HTML标签或者HTML实体,

从而导致页面显示混乱或者存在安全隐患。为了避免这种情况,我们可以使用html.escape()函数来对这些特殊字符进行转义,将它们转换为对应的HTML实体字符编码,从而确保浏览器正确显示文本内容。

例如:

from html import escape

text = 'This is a <b>bold</b> string with special characters like: &copy;'
escaped_text = escape(text)
print(escaped_text)

输出:

This is a &lt;b&gt;bold&lt;/b&gt; string with special characters like: &amp;copy;

在上面的示例中,<b>标签被转义为&lt;b&gt;,&符号被转义为&amp;copy;

这样一来, 浏览器就会将它们显示为普通文本,而不会尝试将它们解析为HTML标签或实体。

因此, html.escape()函数对于防止XSS(跨站脚本)攻击、确保文本内容正确显示等场景非常有用。

 

9.为什么要做这样的替换转化?

我们需要对HTML中的一些特殊字符做转义处理,主要有以下两个原因:

(1) 防止潜在的安全风险

某些特殊字符在HTML/JavaScript中有特殊含义,如果直接将它们插入页面中,可能会被浏览器误解析为HTML标签或JavaScript代码,从而导致潜在的安全隐患,比如跨站脚本攻击(XSS)。

例如,如果我们直接在页面中插入包含<script>标签的文本:

<!-- 未做转义 -->
<span>User input: <script>maliciousCode();</script></span>

浏览器会将<script>标签中的内容当做JavaScript代码执行,从而可能导致XSS攻击。

(2) 确保内容正确显示

有些特殊字符在HTML中有特殊含义,如果不做转义处理,浏览器可能会将它们解析为HTML实体或标签,导致页面显示混乱。

例如,如果我们想在页面中显示<b>Bold</b>这样的文本:

<!-- 未做转义 -->
<p>This is <b>Bold</b> text</p>

浏览器会将<b></b>解析为HTML标签,导致文本被渲染为粗体。

通过html.escape()函数对特殊字符做转义处理后,上面两个例子就变成了:

<!-- 转义后 -->
<span>User input: &lt;script&gt;maliciousCode();&lt;/script&gt;</span>
<p>This is &lt;b&gt;Bold&lt;/b&gt; text</p>

经过转义后,<script>标签和<b>标签都被转换为相应的HTML实体字符,浏览器将不再尝试去解析它们,从而避免了潜在的安全风险,并且可以正确显示出原始文本内容。

因此,对用户输入或其他不可信数据做适当的转义处理是Web应用程序中一个非常重要的环节,可以有效防范XSS等安全问题,并确保页面内容正确显示。

 

 

 10. 如下代码很绕,务必反复多理解吃透。[stacked.py]

def first(f):  # first(inner2nd) # step-3
    print(f'apply first({f.__name__})')  # 2nd print: 'apply first(inner2nd)'

    def inner1st(n):
        result = f(n)  # inner2nd(n) # 3rd print: 'inner2(n): called double(3) -> 6'
        print(f'inner({n}): called {f.__name__}({n}) -> {result}')  # 4th print: inner(3): called inner2nd(3) -> 6
        return result  # 6

    return inner1st  # step-4


def second(f):  # second(double) # step-1
    print(f'apply second({f.__name__})')  # 1st print: 'apply second(double)'

    def inner2nd(n):
        result = f(n)  # double(n)
        print(f'inner2(n): called {f.__name__}({n}) -> {result}')
        return result

    return inner2nd  # step-2


@first
@second
def double(n):
    return n * 2


print(double(3))
"""
#output:

apply second(double)
apply first(inner2nd)
inner2(n): called double(3) -> 6
inner(3): called inner2nd(3) -> 6
6
"""


def double_(n):
    return n * 2


double_ = first(second(double_))
"""
#output:

apply second(double)
apply first(inner2nd)
apply second(double_)
apply first(inner2nd)
"""

对于 print(double(3)) 而言:

我们从底部的装饰器应用开始说起:

  1. @second 先被应用到 double 函数上。second 装饰器定义了一个内部函数 inner2nd。当执行 second(double) 时,会打印 apply second(double)。然后,inner2nd 被返回,它充当了新的被装饰函数。
  2. @first 紧接着被应用到由 @second 返回的 inner2nd 函数上。first 装饰器定义了一个内部函数 inner1st。当执行 first(inner2nd) 时,会打印 apply first(inner2nd)。然后,inner1st 被返回,成为最终被装饰的函数。
  3. 因此,当我们调用 print(double(3)) 时,实际上是在调用 inner1st(3)
  4. 在 inner1st 中,首先执行 result = f(n)。这里 f 是指传递给 first 装饰器的函数,即 inner2nd。所以它调用了 inner2nd(3)
  5. 在 inner2nd 中,同样执行 result = f(n)。这里 f 是指传递给 second 装饰器的函数,即原始的 double 函数。所以它调用了 double(3)。由于 double(n) 的定义是 return n * 2,因此 double(3) 返回 6。这时打印出 inner2(n): called double(3) -> 6
  6. 现在 inner2nd 返回 6 给 inner1st。在 inner1st 中,打印出 inner(3): called inner2nd(3) -> 6
  7. 最后,inner1st 也返回 6,所以最终打印出 6

总结一下,输出顺序是这样的:

  1. apply second(double) - 应用第二个装饰器
  2. apply first(inner2nd) - 应用第一个装饰器
  3. inner2(n): called double(3) -> 6 - inner2nd 调用原始 double 函数
  4. inner(3): called inner2nd(3) -> 6 - inner1st 调用 inner2nd
  5. 6 - 最终结果

这个输出顺序完全遵循了装饰器的应用顺序和内部函数的嵌套调用顺序。装饰器是由内向外应用的,内部函数是由外向内调用的。

所以先应用 @second,然后是 @first。调用时,先调用最外层的 inner1st,然后是 inner2nd,最后是原始的 double 函数。希望这个解释能让你更好地理解输出顺序的来源。

 

追问:为什么第7步,最后,inner1st 也返回 6,所以最终打印出 6

好的,让我解释一下为什么最后 inner1st 返回 6,从而导致最终打印出 6

在第 5 步中,我们提到 inner2nd(3) 最终调用了原始的 double(3),返回值为 6

接着在 inner2nd 函数中,它有一行代码:

return result

其中 result 的值是 6。所以 inner2nd(3) 返回了 6

然后回到 inner1st 函数中,有这一行代码:

result = f(n)

在这里, f 是指传给 first 装饰器的函数参数,也就是 inner2ndn 的值是 3。所以这行代码实际上是 result = inner2nd(3)。而我们已经知道,inner2nd(3) 的返回值是 6

因此,在 inner1st 中, result 的值被赋为 6

接下来,inner1st 函数中有这一行:

return result

由于 result 现在的值是 6,所以 inner1st 函数返回了 6

最后,当我们调用 print(double(3)) 时,实际上是调用了 inner1st(3)。因为 inner1st(3) 返回的就是 6,所以最终打印出来的就是 6

总结一下,inner1st 能够返回 6 的原因是:

  1. 它调用了 inner2nd(3)
  2. inner2nd(3) 又调用了原始的 double(3) 返回 6
  3. inner2nd(3) 将这个 6 返回给了 inner1st
  4. 所以在 inner1st 中, result 的值为 6
  5. inner1st 再将这个 6 返回

 

 11. 动态作用域与词法作用域的概念、和异同之处理解。

动态作用域(Dynamic Scope)词法作用域(Lexical Scope)是编程语言中两种不同的作用域规则,它们决定了变量的可见范围和查找方式。下面是它们在Python中的概念、作用、通俗解释、举例说明以及异同之处。

动态作用域(Dynamic Scope):

  • 概念:根据函数调用的时间顺序来查找变量,而不是根据代码结构。
  • 作用:允许在运行时动态修改变量的绑定,提供了更大的灵活性。
  • 通俗解释:就像找一个人,你不是按照地址去找,而是根据这个人最近去过的地方去找。
  • Python举例:Python本身不支持动态作用域,但我们可以模拟它的行为。
variables = {}

def set_variable(name, value):
    variables[name] = value

def get_variable(name):
    return variables.get(name, None)

def foo():
    value = get_variable('value')
    print(value)

def bar():
    set_variable('value', 2)
    foo()

set_variable('value', 1)
bar() # 输出 2

在这个例子中,我们使用一个全局字典variables来存储变量及其值。当调用bar()时,它先将value设置为2,然后调用foo()。在foo()内部,它查找名为value的变量,找到的是刚刚在bar()中设置的值2

词法作用域(Lexical Scope):

  • 概念:根据代码结构的嵌套层次来查找变量,也称为静态作用域
  • 作用:提供了更好的可预测性和可维护性,因为变量的绑定是静态确定的。
  • 通俗解释:就像你在家里找东西,你是按照房间的布局来找,而不是随机找。
  • Python举例:
value = 1

def foo():
    print(value) # 输出 1

def bar():
    value = 2
    foo() # foo 中访问的是外部的 value

bar()

在这个例子中,foo()函数内部访问的value变量是在foo()函数定义时就绑定的外部value变量,它的值为1。即使在bar()函数内部重新定义了一个名为value的变量,它也不会影响foo()内部访问的value变量。

异同之处:

  • 动态作用域依赖于函数调用的时间顺序,而词法作用域依赖于代码的结构。
  • 动态作用域提供了更大的灵活性,但可能导致代码难以理解和维护。词法作用域更易于推理和维护。
  • Python采用词法作用域规则,这也是大多数现代编程语言所使用的作用域规则。动态作用域在Python中没有得到广泛的应用。

总的来说,Python使用词法作用域,因为它提供了更好的可预测性和可维护性。

动态作用域虽然灵活,但容易导致代码混乱和难以理解,所以在Python中没有得到广泛的应用。词法作用域更加普遍和推荐使用,因为它更易于推理和维护。

 

12. @staticmethod方法的理解,以及和@classmethod的区别。【解释的不错】

staticmethod 是一个 Python 装饰器,用于定义静态方法。静态方法不依赖于类的实例,因此可以直接通过类名调用,而无需创建对象实例。静态方法通常用于实现那些不需要访问实例属性或类属性的功能函数。

概念:

静态方法是一个不依赖于类实例或类属性的普通函数,它们属于类的命名空间,并与类相关联,但不需要通过类实例化即可调用。

作用:

  1. 提供一种组织代码的方式,将相关功能函数放置在类的内部。
  2. 用于实现那些不需要访问实例属性或类属性的功能函数。
  3. 提高代码的可读性和可维护性。

通俗解释:

静态方法就像是一个放在类里面但与类无关的普通函数,它们只需要类的名字即可调用,就像调用一个类的工具箱里的工具一样。

举例说明:

class Math:
    @staticmethod
    def add(x, y):
        return x + y
    
    @staticmethod
    def subtract(x, y):
        return x - y

# 调用静态方法,无需创建类的实例
print(Math.add(5, 3))  # 输出: 8
print(Math.subtract(5, 3))  # 输出: 2

@classmethod 的区别:

  1. @staticmethod 不需要类实例或类属性,而 @classmethod 需要类作为第一个参数。
  2. @staticmethod 被装饰的方法可以通过类名直接调用,而 @classmethod 被装饰的方法需要通过类或类的实例调用。
  3. @staticmethod 不会自动传递任何额外的参数,而 @classmethod 会自动传递类本身作为第一个参数。

 

posted @ 2024-04-10 11:00  AlphaGeek  阅读(20)  评论(0)    收藏  举报