咱们先探讨在Python中如何将参数传递给函数的相干细节,而后回顾与这些概念相干的良好软件工程实际的个别实践。
通过理解Python提供的解决参数的多种形式,咱们可能更轻松地把握通用规定,进而能够轻松地得出结论,即什么是好的模式或习惯用法。而后,咱们能够确定在哪些状况下Python办法是正确的,以及在哪些状况下可能滥用了该语言的个性。
1.如何将参数复制到函数中
Python中的第一条规定是所有参数都由一个值传递——总是这样。这意味着,当把值传递给函数时,它们被调配给稍后将在其上应用的函数签名定义上的变量。你将留神到,函数更改参数可能依赖于类型参数——如果咱们传递可变对象,而函数体批改了这一点,那么,这当然是有副作用的,当函数返回时,它们曾经更改了。
通过如下示例,咱们能够看到其中的区别:
>>> def function(argument):... argument += " in function"... print(argument)...>>> immutable = "hello">>> function(immutable)hello in function>>> mutable = list("hello")>>> immutable'hello'>>> function(mutable)['h', 'e', 'l', 'l', 'o', ' ', 'i', 'n', ' ', 'f', 'u', 'n', 'c', 't', 'i','o', 'n']>>> mutable['h', 'e', 'l', 'l', 'o', ' ', 'i', 'n', ' ', 'f', 'u', 'n', 'c', 't', 'i','o', 'n']>>>
这看起来可能不统一,但事实并非如此。当咱们传递第一个参数(一个字符串)时,它被调配给函数上的参数。因为string对象是不可变的,因而像“argument += <expression>”这样的语句实际上会创立新对象“argument + <expression>”,并将其赋值给参数。此时,argument只是函数范畴内的一个局部变量,与调用方中的原始变量无关。
此外,当咱们传递list时,它是一个可变对象,那么这个语句就有了不同的含意(它实际上等价于在那个list上调用.extend())。该操作符的作用是对一个蕴含原始list对象援用的变量就地批改list,从而批改它。
在解决这些类型的参数时,咱们必须小心,因为它们可能导致意想不到的副作用。除非你相对确定以这种形式操纵可变参数是正确的,否则应防止应用它,并寻找没有这些问题的代替办法。
不要扭转函数参数。一般来说,应尽量避免函数中的副作用。
与许多其余编程语言一样,Python中的参数能够通过地位传递,也能够通过关键字传递。这意味着咱们能够明确地通知函数咱们想要为它的哪个参数设置哪个值。惟一须要留神的是,在通过关键字传递参数之后,前面的其余参数也必须以这种形式传递,否则会引发SyntaxError异样。
2.参数的变量数
Python和其余语言一样,具备内置的函数和构造,这些函数和构造能够接管可变数量的参数。思考这样一种假如,遵循相似C语言中printf函数构造的字符串插值函数(无论是通过应用%运算符还是字符串的格式化办法),第一个地位搁置字符串类型参数,紧随其后的是任意数量的参数,这些参数将被搁置在标记了的格式化字符串中。
除了应用Python中提供的函数外,咱们还能够本人创立函数,这两种函数的应用形式相似。在本节中,咱们将介绍可变参数函数的基本原理,同时给出一些倡议。在下一节中,咱们将探讨当函数参数过多时如何利用这些特色来解决常见的问题和束缚。
对于地位参数的可变数量,在包装这些参数的变量名之前,应用星号(*)。这是通过Python的打包机制实现的。
假如有一个函数有3个地位参数。在某段代码中,咱们恰好能够很不便地将传递给函数的参数存储到一个列表中,列表的元素和函数的参数程序统一。咱们能够应用打包机制,通过一条指令的形式一起传递这些参数,而不是一个一个传递它们(就是说,将列表索引0中的元素传递给第一个参数,将列表索引1中的元素传递给第二个参数,并以此类推),一个一个传递参数的形式十分不合乎Python的格调。
>>> def f(first, second, third):... print(first)... print(second)... print(third)...>>> l = [1, 2, 3]>>> f(*l)123
打包机制的益处是它也能够反过来工作。如果咱们想将一个列表的值按其各自的地位提取到变量中,就能够这样调配它们:
>>> a, b, c = [1, 2, 3]>>> a1>>> b2>>> c3
局部解包也是可能的。假如咱们只对序列的第一个值感兴趣(能够是列表、元组或其余货色),在某个点之后,咱们只心愿其余的值放在一起。咱们能够调配所须要的变量,把其余的放在一个打包列表中。解包的程序是不受限制的。如果在解包的局部没有任何内容能够搁置,那么后果是一个空列表。咱们激励你在Python终端上尝试一些示例,如上面的清单所示,并摸索解包与生成器的关系:
>>> def show(e, rest):... print("Element: {0} - Rest: {1}".format(e, rest))...>>> first, *rest = [1, 2, 3, 4, 5]>>> show(first, rest)Element: 1 - Rest: [2, 3, 4, 5]>>> *rest, last = range(6)>>> show(last, rest)Element: 5 - Rest: [0, 1, 2, 3, 4]>>> first, *middle, last = range(6)>>> first0>>> middle[1, 2, 3, 4]>>> last5>>> first, last, *empty = (1, 2)>>> first1>>> last2>>> empty[]
在迭代中能够找到解包变量的最佳用处之一。当咱们必须遍历一组元素,而每个元素又顺次是一个序列时,最好是在遍历每个元素的同时解包。为了理论查看这样的示例,咱们将假如有一个函数用来接管数据库行的列表,并负责从该数据创立用户。第一个要实现的是,从行中每一列的地位获取要结构用户的值,这基本不是习惯用法。第二个要实现的是,在迭代时进行解包:
USERS = [(i, f"first_name_{i}", "last_name_{i}") for i in range(1_000)]class User: def __init__(self, user_id, first_name, last_name): self.user_id = user_id self.first_name = first_name self.last_name = last_namedef bad_users_from_rows(dbrows) -> list: """A bad case (non-pythonic) of creating ``User``s from DB rows.""" return [User(row[0], row[1], row[2]) for row in dbrows]def users_from_rows(dbrows) -> list: """Create ``User``s from DB rows.""" return [ User(user_id, first_name, last_name) for (user_id, first_name, last_name) in dbrows ]
能够留神到,第二个版本更容易浏览。在第一个版本的函数(bad_users_from_rows)中,有以row[0]、row[1]和row[2]的模式示意的数据,这些数据并没有阐明它们是什么。换句话说,像user_id、first_name和last_name这样的变量代表了它们本人。
在设计函数时,咱们能够利用这种性能。
咱们在规范库中能够找到的一个示例是max函数,它的定义如下:
max(...) max(iterable, *[, default=obj, key=func]) -> value max(arg1, arg2, *args, *[, key=func]) -> value With a single iterable argument, return its biggest item. The default keyword-only argument specifies an object to return if the provided iterable is empty. With two or more arguments, return the largest argument.
其中有一个相似的表示法,关键字参数用两个星号(**)示意。如果有一个字典,咱们用两个星号把它传递给一个函数,那么该函数会选择键作为参数的名称,而后把键的值作为函数中那个参数的值进行传递。
例如,上面这行代码:
function(**{"key": "value"})
等同于:
function(key="value")
相同,如果咱们定义一个参数以两个星号结尾的函数,则将产生相同的状况——关键字提供的参数将被打包到字典中:
>>> def function(**kwargs):... print(kwargs)...>>> function(key="value"){'key': 'value'}
函数中参数的数量
咱们认为,如果函数或办法中的参数太多,就意味着代码设计很蹩脚(“代码异味”)。鉴于此,咱们将给出这个问题的解决方案。
一种解决方案是软件设计的一个更通用的准则——具体化(为传递的所有参数创立一个新对象,这可能是咱们短少的形象)。将多个参数压缩到一个新对象中并不是Python特有的解决方案,而是能够利用到任何编程语言中。
另一种解决方案是应用咱们在上一节中看到的特定于Python的个性,利用变量地位参数和关键字参数创立具备动静签名的函数。尽管这可能是一种Python式的解决形式,但咱们必须小心,不要滥用该个性,因为可能创立了一些太过于动静的货色,以至于难以保护。在这种状况下,咱们应该看一下函数的主体。不论签名或者参数是否仿佛是正确的,如果函数应用参数的值做了太多不同的事件,那么这是一个信号——它必须被分解成多个更小的函数。(记住,函数应该做一件事,而且仅做一件事!)
1.函数参数和耦合
函数签名的参数越多,这个参数就越有可能与调用方函数严密耦合。
假如有两个函数f1和f2,函数f2有5个参数。f2接管的参数越多,对于任何试图调用该函数的人来说,收集所有信息并将其传递上来以便使其失常工作的难度就越大。
当初,f1仿佛有所有这些信息,因为这些信息能正确调用f1,由此咱们能够得出两个论断。首先,f2可能是一个有破绽的抽象概念,这意味着,当f1晓得f2所须要的所有货色时,它简直能够晓得本人在做什么,并且可能自行实现。总而言之,f2形象得没那么多。其次,f2看起来只对f1有用,很难设想在不同的上下文中应用这个函数,这使得重用变得更加艰难。
当函数具备更通用的接口并且可能解决更高级别的形象时,它们就变得更加可重用。
这实用于所有类型的函数和对象办法,包含类的__init__办法。这种办法的呈现通常(但并不总是)意味着应该传递一个新的更高层级的形象,或者存在一个缺失的对象。
如果一个函数须要太多的参数能力失常工作,就能够将其看作“代码异味”。
事实上,这是一个设计问题——动态剖析工具,如Pylint(见第1章),在遇到这种状况时,默认会收回正告。如果产生这种状况,不要克制正告,而应该重构它。
2.应用太多参数的简洁函数签名
假如咱们找到一个须要太多参数的函数,并且晓得不能就这样把它搁置在代码库中,必须重构它。然而,用什么办法呢?
依据具体情况,咱们能够利用以下一些规定。这些规定尽管不是宽泛实用的,但能够为咱们解决那些常见问题提供思路。
有时,如果看到大多数参数属于一个公共对象,就能够用一种简略的办法更改参数。例如,思考这样一个函数调用:
track_request(request.headers, request.ip_addr, request.request_id)
当初,函数可能接管或不接管其余参数,但这里有一点非常明显:所有参数都依赖于request,那么为什么不间接传递request对象呢?这是一个简略的更改,然而它显著地改良了代码。正确的函数调用应该是track_request(request)办法。进一步来说,从语义上讲,调用track_request(request)办法也更有意义。
尽管激励传递这样的参数,然而在所有将可变对象传递给函数的状况下,咱们必须十分小心副作用。咱们调用的函数不应该对传递的对象做任何批改,因为这会使对象发生变化,产生不心愿呈现的副作用。除非这实际上是想要的成果(在这种状况下,必须明确阐明),否则不激励这种行为。即便当咱们实际上想要更改正在解决的对象上的某些内容时,更好的代替办法是复制它并返回(新的)批改后的版本。
解决不可变对象,并尽可能防止副作用。
这给咱们带来了一个相似的主题:分组参数。在后面的示例中,咱们曾经对参数进行了分组,然而没有应用组(在本示例中是申请对象)。然而其余状况没有这种状况那么显著,咱们可能心愿将参数中的所有数据分组到能充当容器的单个对象中。不用说,这种分组必须有意义。这里的想法是具体化:创立设计中短少的形象。
如果后面的策略不起作用,作为最初的伎俩,咱们能够更改函数的签名,以接管可变数量的参数。如果参数的数量太多,应用args或*kwargs会使事件更加难以了解,所以咱们必须确保接口被正确地记录和应用,但在某些状况下,这是值得做的。
确实,用args和*kwargs定义的函数非常灵活且适应性强,但毛病是失去了它的签名,以及它的局部含意和简直所有易读性。咱们曾经看到了变量名(包含函数参数)如何使代码更容易浏览的示例。如果一个函数将获取任意数量的参数(地位或关键字),当咱们想要看看这个函数在将来能够做什么时,咱们可能无奈通过这些参数理解到这一点,除非有一个十分好的文档阐明。
以上就是本次分享的全部内容,当初想要学习编程的小伙伴欢送关注Python技术大本营,获取更多技能与教程。