一、说明
读完标题后,你可能会问自己一些类似的事情,“Python 中的函数是一个高级概念?如何?所有课程都引入了功能作为语言的基本块。你既是对的,也是错的。
大多数关于 Python 的课程都将函数作为基本概念和构建块进行介绍,因为没有它们,您将根本无法编写函数式代码。这与函数式编程范式完全不同,函数式编程范式是一个单独的概念,但我也将触及这个概念。
在我们深入研究 Python 函数的高级复杂性之前,让我们简要回顾一些基本概念和您可能已经知道的事情。
二、简要基础知识
所以你开始编写你的程序,在某个时候你最终会写出相同的代码序列。你开始重复自己和代码块。事实证明,这是引入功能的好时机和地点。至少,是这样。在 Python 中,将函数定义为:
def shout(name):
print(f'Hey! My name is {name}.')
在软件工程领域,我们区分了功能定义的各个部分:
def
- 用于定义函数的 Python 关键字。shout
- 函数名称。shout(name)
- 函数声明。name
- 函数参数。print(...)
是函数体的一部分,或者我们如何称呼它为函数定义。
一个函数可以返回一个值,也可以根本没有返回值,就像我们之前定义的那个。当函数返回值时,它可以返回一个或多个:
def break_sentence(sentence):
return sentence.split(' ')
结果是一个元组,您可以解压缩或选择任何元组元素来继续。
对于那些不知情的人来说,Python 中的函数是一等公民。这是什么意思?这意味着您可以像使用任何其他变量一样使用函数。您可以将它们作为参数传递给其他函数,从函数返回它们,甚至将它们存储在变量中。下面是其中一个示例:
def shout(name):
return f'Hey! My name is {name}.'
# we will use break_sentence defined above
# assign function to another variable
another_breaker = break_sentence
another_breaker(shout('John'))
# ['Hey!', 'My', 'name', 'is', 'John.']
# Woah! Yes, this is a valid way to define function
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 name
def shout(name, decorator=no_decorator):
decorated_name = decorator(name)
return f'Hey! My name is {decorated_name}'
shout('John')
# 'Hey! My name is John'
shout('John', decorator=dash_decorator)
# 'Hey! My name is J-o-h-n'
这就是将lambda
函数作为参数传递给另一个函数的样子。功能呢?好吧,看看下面的例子:
def shout(name, decorator=lambda x: x):
decorated_name = decorator(name)
return f'Hey! My name is {decorated_name}'
print(shout('John'))
# Hey! My name is John
print(shout('John', decorator=dash_decorator))
# Hey! My name is J-o-h-n
现在,默认的装饰函数是 lambda
并按原样返回参数的值(幂等)。在这里,它是匿名的,因为它没有附加名称。
请注意, print 也是一个函数,我们在其中传递一个函数作为参数。本质上,我们是链接函数。这可以引导我们走向函数式编程范式,这是你可以在 Python 中选择的路径。我将尝试专门针对这个主题写另一篇博文,因为它对我来说非常有趣。现在,我们将保持过程式编程范例;也就是说,我们将继续我们迄今为止所做的事情。
如前所述,函数可以分配给变量,作为参数传递给另一个函数,并从该函数返回。我已经向您展示了前两种情况的一些简单示例,但是从函数返回函数怎么样?起初我想让它非常简单,但话又说回来,这是一个高级 Python!
三、中级或高级零件
这绝不是 Python 中函数和函数高级概念的指南。有很多很棒的材料,我将在这篇文章的最后留下。但是,我想谈谈我发现非常有趣的几个有趣的方面。
Python 中的函数是对象。我们怎样才能弄清楚这一点? Python 中的每个对象都是一个类的实例,该类最终继承自一个称为 type 的特定类。其细节很复杂,但为了能够了解这与函数有什么关系,这里有一个示例:
type(shout)
# function
type(type(shout))
# type
当您在 Python 中定义一个类时,它会自动继承该类。继承哪个类?object
object
type(object)
# type
我应该告诉你 Python 中的类也是对象吗?事实上,这对初学者来说是令人难以置信的。但正如吴恩达所说,这并不那么重要;别担心。
好的,所以函数是对象。当然,函数应该有一些神奇的方法,对吧?
shout.__class__
# function
shout.__name__
# shout
shout.__call__
# <method-wrapper '__call__' of function object at 0x10d8b69e0>
# Oh snap!
魔术方法 __call__ 是为可调用的对象定义的。所以我们的shout对象(函数)是可调用的。我们可以带或不带参数调用它。但这很有趣。我们之前所做的是定义一个喊叫函数,并获取一个可以使用 __call__ 魔术方法调用的对象,该方法是一个函数。你看过电影《盗梦空间》吗?
所以,我们的函数实际上并不是一个函数,而是一个对象。对象是类的实例并包含方法和属性,对吗?这是您应该从 OOP 中了解的内容。我们怎样才能知道对象的属性是什么?有一个名为 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函数的范围内无法访问。这样,您就无法轻松测试它,这是这种方法的另一个问题。
然而,在一种特定情况下,在另一个函数中定义一个函数是一种可行的方法。这是当你实现函数的装饰器时。这与我们在前面的示例之一中用来修饰名称字符串的函数无关。
四、Python 中的装饰器函数
什么是装饰器函数?可以把它看作是包装函数的函数。这样做的目的是为现有函数引入其他功能。例如,假设您希望在每次调用函数时进行记录:
def my_function():
return sum(range(10))
def my_logger(fun):
print(f'{fun.__name__} is being called!')
return fun
my_function()
# 45
my_logger(my_function)
# my_function is being called!
# <function my_function at 0x105afbeb0>
my_logger(my_function)()
# my_function is being called!
# 45
注意我们如何装饰我们的功能;我们将其作为参数传递给装饰参数。但这还不够!请记住,装饰器返回函数,并且需要调用(调用)此函数。这是最后一次调用的作用。
现在,在实践中,您真正想要的是装饰保留在原始函数的名称下。在我们的例子中,我们希望在解释器解析我们的代码之后,是修饰函数的名称。这样一来,我们就能让事情变得简单易懂,并且我们确保代码的任何部分都无法调用函数的未修饰版本。例:my_function
def my_function():
return sum(range(10))
def my_logger(fun):
print(f'{fun.__name__} is being called!')
return fun
my_function = my_logger(my_function)
my_function(10)
# my_function is being called!
# 45
您会承认,我们将函数名称重新分配给装饰名称的部分很麻烦。你必须牢记这一点。如果要记录许多函数调用,则会有很多重复代码。这就是句法糖的用武之地。定义修饰器函数后,可以使用它来修饰另一个函数,方法是在函数定义前面加上修饰器函数的名称。例:@
def my_logger(fun):
print(f'{fun.__name__} is being called!')
return fun
@my_logger
def my_function():
return sum(range(10))
my_function()
# my_function is being called!
# 45
这是 Python 的禅宗。看看代码的表现力和简单性。
这里有一件重要的事情需要注意!尽管输出有意义,但这不是您所期望的!在加载 Python 代码时,解释器将调用该函数并有效地运行它!您将获得日志输出,但这不会是我们首先想要的。现在看代码:my_logger
def my_logger(fun):
print(f'{fun.__name__} is being called!')
return fun
@my_logger
def my_function():
return sum(range(10))
my_function()
# my_function is being called!
# 45
my_function()
# 45
为了能够在调用原始函数后运行装饰器代码,我们必须将其包装在另一个函数周围。这就是事情可能会变得混乱的地方。下面是一个示例:
def my_logger(fun):
def _inner_decorator(*args, **kwargs):
print(f'{fun.__name__} is being called!')
return fun(*args, **kwargs)
return _inner_decorator
@my_logger
def my_function(n):
return sum(range(n))
print(my_function(5))
# my_function is being called!
# 10
在此示例中,也有一些更新,因此让我们回顾一下它们:
- 我们希望能够将参数传递给 my_function。
- 我们希望能够装饰任何函数,而不仅仅是 my_function。因为我们不知道未来函数的参数的确切数量,所以我们必须尽可能保持通用性,这就是我们使用 *args 和 **kwargs 的原因。
- 最重要的是,我们定义了 _inner_decorator,每次在代码中调用 my_function 时都会调用它。它接受位置参数和关键字参数,并将它们作为参数传递给修饰函数。
始终记住,装饰器函数必须返回一个函数,该函数接受相同的参数(数字及其各自的类型)并返回相同的输出(同样,数字及其各自的类型)。也就是说,如果你想让函数用户不感到困惑,代码阅读器不试图弄清楚到底发生了什么。
例如,假设您有两个结果不同的函数,但也需要参数:
@my_logger
def my_function(n):
return sum(range(n))
@my_logger
def my_unordinary_function(n, m):
return sum(range(n)) + m
print(my_function(5))
# my_function is being called!
# 10
print(my_unordinary_function(5, 1))
# my_unordinary_function is being called!
# 11
在我们的示例中,装饰器函数只接受它装饰的函数。但是,如果要传递其他参数并动态更改装饰器行为,该怎么办?假设您要调整记录器装饰器的详细程度。到目前为止,我们的装饰器函数已经接受了一个参数:它装饰的函数。但是,当装饰器函数有自己的参数时,这些参数将首先传递给它。然后,装饰器函数必须返回一个接受修饰函数的函数。从本质上讲,事情变得越来越复杂。还记得电影《盗梦空间》的参考资料吗?
下面是一个示例:
from enum import IntEnum, auto
from datetime import datetime
from functools import wraps
class LogVerbosity(IntEnum):
ZERO = auto()
LOW = auto()
MEDIUM = 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 level: {verbosity}')
print(f'LOG: {fun.__name__} is being called!')
if verbosity >= LogVerbosity.MEDIUM:
print(f'LOG: Date and time of call is {datetime.utcnow()}.')
if verbosity == LogVerbosity.HIGH:
print(f'LOG: Scope of the caller is {__name__}.')
print(f'LOG: Arguments are {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)) + m
print(my_function(10))
# LOG: Verbosity level: LOW
# LOG: my_function is being called!
# 45
print(my_unordinary_function(5, 1))
# LOG: Verbosity level: HIGH
# LOG: my_unordinary_function is being called!
# LOG: Date and time of call is 2023-07-25 19:09:15.954603.
# LOG: Scope of the caller is __main__.
# LOG: Arguments are (5, 1), {}
# 11
我不会详细描述与装饰器无关的代码,但我鼓励您查找并学习。这里我们有一个装饰器,以不同的详细程度记录函数调用。如前所述,my_logger 装饰器现在接受动态更改其行为的参数。将参数传递给它后,它返回的结果函数应该接受一个要装饰的函数。这是 _inner_logger 函数。现在,您应该了解装饰器代码的其余部分在做什么。
五、结论
我写这篇文章的第一个想法是写一些高级主题,比如 Python 中的装饰器。但是,正如您现在可能知道的那样,我也提到并使用了许多其他高级主题。在以后的文章中,我将在一定程度上解决其中的一些问题。尽管如此,我对你的建议是,也要从其他来源了解这里提到的事情。如果你正在使用任何编程语言进行开发,掌握这些函数是必须的,但掌握你选择的编程语言的所有方面可以让你在编写代码方面有很大的优势。
我希望我已经为你介绍了一些新的东西,并且你现在对作为高级 Python 程序员编写函数充满信心。
六、引用
- Python 装饰器入门
- Python 内部函数:它们有什么用?
- 枚举 HOWTO