Press "Enter" to skip to content

高级Python:函数

你将如何与Python纠缠在一起。iam_os在Unsplash上的照片

在阅读标题之后,你可能会问自己一些类似于“Python中的函数是高级概念吗?怎么可能?所有的课程都将函数作为语言的基本组成部分介绍。”你既对又错。

大多数Python课程将函数作为基本概念和构建模块介绍,因为没有它们,你将无法编写功能代码。这与函数式编程范式完全不同,但我也会涉及到这个概念。

在我们深入研究Python函数的高级复杂性之前,让我们简要介绍一些基本概念和你可能已经了解的内容。

简要基础知识

你开始编写程序,某个时刻你会发现自己写了相同的代码序列。你开始重复自己和代码块。这是一个很好的时间和地点来介绍函数。至少,就是这样。在Python中,你可以这样定义一个函数:

def shout(name):    print(f'嘿!我的名字是{name}。')

在软件工程领域,我们对函数定义的部分进行区分:

  • def – 用于定义函数的Python关键字。
  • shout – 函数名。
  • shout(name)– 函数声明。
  • name – 函数参数。
  • print(...)是函数体的一部分,或者我们称之为函数定义。

函数可以返回一个值,也可以没有返回值,就像我们之前定义的那个函数一样。当函数返回值时,可以返回一个或多个值:

def break_sentence(sentence):    return sentence.split(' ')

你得到的结果是一个元组,你可以解包它或选择任何元组元素进行后续处理。

对于那些尚未了解的人来说,Python中的函数是“一等公民”。这意味着你可以像处理任何其他变量一样处理函数。你可以将它们作为参数传递给其他函数,从函数中返回它们,甚至将它们存储在变量中。以下是一个例子:

def shout(name):    return f'嘿!我的名字是{name}。'# 我们将使用上面定义的break_sentence# 将函数赋值给另一个变量another_breaker = break_sentence another_breaker(shout('John'))# ['嘿!', '我的', '名字', '是', 'John。']# 哇!是的,这是一种有效的定义函数的方法name_decorator = lambda x: '-'.join(list(name))name_decorator('John')# 'J-o-h-n'

等等,这个lambda是什么?这是Python中另一种定义函数的方式。这是所谓的无名或匿名函数。嗯,在这个例子中,我们将其分配给一个名为name_decorator的变量,但你可以将lambda表达式作为另一个函数的参数,而无需给它命名。我将很快介绍这个。

剩下的就是给出一个例子,展示函数如何作为参数传递或从另一个函数返回值。这是我们向高级概念迈进的部分,请耐心等待。

def dash_decorator(name):    return '-'.join(list(name))def no_decorator(name):    return namedef shout(name, decorator=no_decorator):    decorated_name = decorator(name)    return f'嘿!我的名字是{decorated_name}'shout('John')# '嘿!我的名字是John'shout('John', decorator=dash_decorator)# '嘿!我的名字是J-o-h-n'

这就是将函数作为参数传递给另一个函数的方式。那么lambda函数呢?好吧,看看下一个例子:

def shout(name, decorator=lambda x: x):    decorated_name = decorator(name)    return f'嘿!我的名字是{decorated_name}'print(shout('John'))# 嘿!我的名字是Johnprint(shout('John', decorator=dash_decorator))# 嘿!我的名字是J-o-h-n

现在默认的装饰函数是lambda,并且返回参数的值本身(幂等性)。在这里,它是匿名的,因为它没有附加的名称。

请注意,print也是一个函数,我们将一个函数shout作为参数传递给它。本质上,我们在链接函数。这可以引导我们进入函数式编程范式,这是您可以在Python中选择的一种路径。我将尝试撰写另一篇专门介绍这个主题的博文,因为我对它非常感兴趣。目前,我们将继续使用过程式编程范式;也就是说,我们将继续做我们迄今为止所做的事情。

如前所述,函数可以分配给变量,作为另一个函数的参数传递,并从该函数返回。我已经给您展示了前两种情况的一些简单示例,但是如果从函数中返回一个函数呢?起初我想保持非常简单,但话又说回来,这是高级Python!

中级或高级部分

这绝不是关于Python函数和高级概念的”THE”指南。有很多很棒的资料,我将在本文末尾留下。然而,我想谈谈我发现非常有趣的一些方面。

在Python中,函数是对象。我们如何弄清楚这一点呢?嗯,Python中的每个对象都是从一个特定的类继承的类的实例,这个类叫做type。这其中的细节很复杂,但为了能够看到这与函数有什么关系,这里有一个例子:

type(shout)# functiontype(type(shout))# type

当您在Python中定义一个类时,它会自动继承object类。而object类又继承了哪个类呢?

type(object)# type

我应该告诉您Python中的类也是对象吗?确实,对于初学者来说,这是令人费解的。但正如Andrew Ng所说,这并不重要,不要担心它。

好了,所以函数是对象。那么函数应该有一些魔术方法,对吗?

shout.__class__# functionshout.__name__# shoutshout.__call__# <method-wrapper '__call__' of function object at 0x10d8b69e0># 噢,嘭!

魔术方法__call__是为可调用的对象定义的。所以我们的shout对象(函数)是可调用的。我们可以带参数或不带参数调用它。但这很有趣。我们之前所做的是定义了一个shout函数,并获得了一个可调用的对象,该对象具有__call__魔术方法,它是一个函数。您看过电影《盗梦空间》吗?

因此,我们的函数实际上不是一个函数,而是一个对象。对象是类的实例,包含方法和属性,对吗?这是您应该从面向对象编程中了解的内容。我们如何找出我们的对象的属性是什么?有这样一个名为vars的Python函数,它返回一个带有对象属性及其值的字典。让我们看看下一个示例中会发生什么:

vars(shout)# {}shout.name = 'Jimmy'vars(shout)# {'name': 'Jimmy'}

这很有趣。不是说您可以立即找到这种用例。即使您找到了,我也强烈不建议您进行这种黑魔法。尽管它很有趣,但很难跟踪。我之所以向您展示这个,是因为我们想要证明函数确实是对象。请记住,Python中的一切都是对象。这就是我们在Python中的风格。

现在,令人期待的函数返回了。这个概念也非常有趣,因为它给您带来了很多实用性。通过一点点的语法糖,您可以得到非常有表现力的效果。让我们深入探讨。

首先,一个函数的定义可以包含另一个函数的定义。甚至可以有多个。下面是一个完全正常的例子:

def shout(name):    def _upper_case(s):        return s.upper()    return _upper_case(name)

如果您认为这只是name.upper()的复杂版本,那么您是正确的。但是请稍等,我们正在接近目标。

因此,根据之前的示例,这是完全功能的Python代码,您可以在函数内部定义多个函数进行实验。这种巧妙的技巧的价值是什么呢?嗯,您可能会遇到函数非常庞大,代码块重复的情况。这种情况下,定义子函数会增加代码的可读性。实践中,庞大的函数是代码质量不佳的标志,强烈鼓励将它们拆分为几个较小的函数。因此,遵循这个建议,您很少需要在彼此内部定义多个函数。需要注意的一点是,_upper_case函数是隐藏的,并且在shout函数定义的范围之外,无法调用。这样,您无法轻松地对其进行测试,这是这种方法的另一个问题。

然而,有一种特殊情况可以在另一个函数内部定义函数。这就是实现函数装饰器时的情况。这与我们在之前的示例中用来装饰name字符串的函数无关。

Python中的装饰器函数

什么是装饰器函数?将其视为包装您的函数的函数。这样做的目的是为已有的函数引入额外功能。例如,假设您想要在每次调用函数时记录日志:

def my_function():    return sum(range(10))def my_logger(fun):    print(f'{fun.__name__}正在被调用!')    return funmy_function()# 45my_logger(my_function)# my_function正在被调用!# <function my_function at 0x105afbeb0>my_logger(my_function)()# my_function正在被调用!# 45

请注意,我们如何装饰我们的函数;我们将它作为参数传递给装饰函数。但这还不够!记住,装饰器返回函数,而这个函数需要被调用。这就是最后一次调用的作用。

现在,实际上,您真正希望的是装饰持续存在于原始函数的名称下。在我们的例子中,我们希望在解释器解析我们的代码之后,my_function是装饰函数的名称。这样,我们保持代码简单易懂,并确保我们的代码的任何部分都无法调用未装饰的函数版本。例如:

def my_function():    return sum(range(10))def my_logger(fun):    print(f'{fun.__name__}正在被调用!')    return funmy_function = my_logger(my_function)my_function(10)# my_function正在被调用!# 45

您会发现将函数的名称重新分配为装饰函数的名称部分是麻烦的。您必须记住这一点。如果有多个要记录的函数调用,将会有很多重复的代码。这就是语法糖的用武之地。在定义装饰器函数之后,您可以使用它来装饰另一个函数,方法是在函数定义之前加上@和装饰器函数的名称。例如:

def my_logger(fun):    print(f'{fun.__name__}正在被调用!')    return fun@my_loggerdef my_function():    return sum(range(10))my_function()# my_function正在被调用!# 45

这就是Python的哲学。看看代码的表达力和简洁性。

这里有一件重要的事情要注意!尽管输出是有意义的,但它并不是您所期望的!在加载Python代码时,解释器将调用my_logger函数并有效地运行它!您将获得日志输出,但这不是我们最初想要的结果。现在看看代码:

def my_logger(fun):    print(f'{fun.__name__}正在被调用!')    return fun@my_loggerdef my_function():    return sum(range(10))my_function()# my_function正在被调用!# 45my_function()# 45

为了能够在调用原始函数后运行装饰器代码,我们必须将其包装在另一个函数周围。这就是事情可能变得混乱的地方。以下是一个示例:

def my_logger(fun):    def _inner_decorator(*args, **kwargs):        print(f'{fun.__name__} 正在被调用!')        return fun(*args, **kwargs)    return _inner_decorator@my_loggerdef my_function(n):    return sum(range(n))print(my_function(5))# my_function 正在被调用!# 10

在这个例子中,还有一些更新,我们来看一下:

  1. 我们希望能够将参数传递给my_function
  2. 我们希望能够装饰任何函数,而不仅仅是my_function。因为我们不知道未来函数的确切参数数量,所以我们必须尽可能地保持通用,这就是为什么我们使用*args**kwargs
  3. 最重要的是,我们定义了_inner_decorator,它将在每次我们在代码中调用my_function时被调用。它接受位置参数和关键字参数,并将它们作为参数传递给被装饰的函数。

请始终记住,装饰器函数必须返回一个接受相同参数(数量和类型)并返回相同输出(同样是数字和它们的相应类型)的函数。也就是说,如果你想让函数的用户不困惑,代码阅读者不需要试图弄清楚到底发生了什么。

例如,假设你有两个结果不同但需要参数的函数:

@my_loggerdef my_function(n):    return sum(range(n))@my_loggerdef my_unordinary_function(n, m):    return sum(range(n)) + mprint(my_function(5))# my_function 正在被调用!# 10print(my_unordinary_function(5, 1))# my_unordinary_function 正在被调用!# 11

在我们的例子中,装饰器函数只接受它装饰的函数作为参数。但是,如果你想传递额外的参数并动态更改装饰器的行为怎么办?比如你想调整日志记录器装饰器的详细程度。到目前为止,我们的装饰器函数接受了一个参数:它装饰的函数。然而,当装饰器函数有自己的参数时,这些参数首先传递给它。然后,装饰器函数必须返回一个接受被装饰函数的函数。基本上,事情变得更加复杂了。还记得电影《盗梦空间》的参考吗?

下面是一个例子:

from enum import IntEnum, autofrom datetime import datetimefrom functools import wrapsclass LogVerbosity(IntEnum):    ZERO = auto()    LOW = auto()    VoAGI = auto()    HIGH = auto()def my_logger(verbosity: LogVerbosity):    def _inner_logger(fun):        def _inner_decorator(*args, **kwargs):            if verbosity >= LogVerbosity.LOW:                print(f'LOG: 详细程度等级: {verbosity}')                print(f'LOG: {fun.__name__} 正在被调用!')            if verbosity >= LogVerbosity.MEDIUM:                print(f'LOG: 调用日期和时间为 {datetime.utcnow()}。')            if verbosity == LogVerbosity.HIGH:                print(f'LOG: 调用者的作用域为 {__name__}。')                print(f'LOG: 参数为 {args}, {kwargs}')            return fun(*args, **kwargs)        return _inner_decorator    return _inner_logger@my_logger(verbosity=LogVerbosity.LOW)def my_function(n):    return sum(range(n))@my_logger(verbosity=LogVerbosity.HIGH)def my_unordinary_function(n, m):    return sum(range(n)) + mprint(my_function(10))# LOG: 详细程度等级: LOW# LOG: my_function 正在被调用!# 45print(my_unordinary_function(5, 1))# LOG: 详细程度等级: HIGH# LOG: my_unordinary_function 正在被调用!# LOG: 调用日期和时间为 2023-07-25 19:09:15.954603。# LOG: 调用者的作用域为 __main__。# LOG: 参数为 (5, 1), {}# 11 

我不会详细描述与装饰器无关的代码,但我鼓励你查阅并学习。这里我们有一个记录函数调用的装饰器,具有不同的详细程度。如前所述,my_logger装饰器现在接受可以动态更改其行为的参数。参数传递给它后,它返回的结果函数应该接受一个需要装饰的函数。这就是_inner_logger函数。到目前为止,你应该明白装饰器代码的其余部分是在做什么。

结论

我对这篇文章的第一个想法是写关于Python中装饰器等高级主题。然而,正如你现在可能已经知道的,我还提到并使用了许多其他高级主题。在以后的文章中,我将对其中一些进行一定程度的探讨。然而,我给你的建议是从其他来源也学习这里提到的内容。掌握函数是任何编程语言开发中必须要做到的,但掌握你选择的编程语言的所有方面,可以让你在编写代码时获得很大的优势。

我希望我为你介绍了一些新东西,并且你现在对作为一名高级Python程序员编写函数有信心。

参考资料

  • Python装饰器入门
  • Python内部函数:它们有什么用?
  • 枚举(Enum)使用指南
Leave a Reply

Your email address will not be published. Required fields are marked *