我们首先探讨如何将参数传递给Python中的函数的细节,然后回顾与这些概念相关的良好软件工程实践的一般理论。通过了解Python提供的多种处理参数的方式,我们可以更容易地掌握一般规则,进而可以轻松得出什么是好的模式或习惯用法的结论。然后我们可以确定Python方法在哪些情况下是正确的,在哪些情况下可能滥用了该语言的特性。1.如何将参数复制到函数中Python中的第一条规则是所有参数都按值传递——始终如此。这意味着当值被传递给一个函数时,它们被分配给函数签名上定义的变量,这些变量将在以后使用。您会注意到函数更改可能取决于类型参数的参数-如果我们传递可变对象并且函数体修改了它,那么,当然,这会产生副作用,并且当函数返回时,它们已经改变。通过下面的例子,我们可以看出区别:>>>deffunction(argument):...argument+="infunction"...print(argument)...>>>immutable="hello">>>>function(mutable)helloinfunction>>>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']>>>这看起来不一致,但事实并非如此。当我们传递第一个参数(字符串)时,它被分配给函数上的参数。由于字符串对象是不可变的,像“argument+=”这样的语句实际上创建了一个新对象“argument+”,并将其分配给参数。此时,argument只是函数作用域内的一个局部变量,与调用者中的原始变量无关。此外,当我们传递list时,它是一个可变对象,则此语句具有不同的含义(它实际上等同于在该列表上调用.extend())。此运算符所做的是在包含对原始列表对象的引用的变量上就地修改列表,从而修改它。在处理这些类型的参数时我们必须小心,因为它们可能会导致意想不到的副作用。除非您绝对确定以这种方式操纵可变参数是正确的做法,否则请避免使用它并寻找没有这些问题的替代方案。不要更改功能参数。一般来说,应该尽可能避免函数中的副作用。与许多其他编程语言一样,Python中的参数可以按位置或按关键字传递。这意味着我们可以明确地告诉函数我们要为它的哪个参数设置哪个值。唯一需要注意的是,通过关键字传递一个参数后,其他参数也必须通过这种方式传递,否则会抛出SyntaxError异常。2.可变数量的参数Python与其他语言一样,具有可以采用可变数量参数的内置函数和结构。考虑一个假设,后面是一个类似于C中printf函数结构的字符串插值函数(无论是使用%运算符还是字符串的format方法),字符串类型参数放在第一个位置,后面是any将放置在标记的格式字符串中的参数数量。除了使用Python提供的函数外,我们还可以创建自己的函数,使用方法类似。在本节中,我们介绍可变参数函数的基础知识并给出一些建议。在下一节中,我们将探讨如何利用这些特性来处理函数参数过多时的常见问题和约束。对于可变数量的位置参数,在包装这些参数的变量名前使用星号(*)。这是通过Python的打包机制实现的。假设有一个带有3个位置参数的函数。在某段代码中,我们可以方便地将传递给函数的参数存储在一个列表中,列表中的元素与函数的参数顺序相同。我们可以使用打包机制在一条指令中将这些参数一起传递,而不是一个一个地传递它们(即将列表的索引0处的元素传递给第一个参数,将索引1处的元素传递给第二个参数)争论)。两个参数,等等),一个一个传递参数的方式很unpythonic。>>>deff(第一,第二,第三):...打印(第一)...打印(第二)...打印(第三)...>>>l=[1,2,3]>>>f(*l)123打包机制的好处在于它也可以反向工作。如果我们想把一个列表的值按照各自的位置提取到变量中,我们可以这样赋值:>>>a,b,c=[1,2,3]>>>a1>>>b2>>也可以对c3进行部分解包。假设我们只对序列的第一个值感兴趣(可以是列表、元组或其他东西),在某个点之后我们只希望将其余值放在一起。我们可以分配我们需要的变量并将其余的放在打包列表中。开箱顺序不限。如果没有任何东西可以放入解包部分,则结果是一个空列表。我们鼓励您在Python终端上尝试一些示例,如下面的清单所示,并探索解包与生成器的关系:>>>defshow(e,rest):...print("Element:{0}-休息:{1}".format(e,rest))...>>>首先,*rest=[1,2,3,4,5]>>>show(first,rest)Element:1-休息:[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}")foriinrange(1_000)]classUser:def__init__(self,user_id,first_name,last_name):self.user_id=user_idself.first_name=first_nameself.last_name=last_namedefbad_users_from_rows(dbrows)->list:"""从数据库创建``User``的糟糕案例(非pythonic)rows."""return[User(row[0],row[1],row[2])forrowindbrows]defusers_from_rows(dbrows)->list:"""从DB创建``User``rows."""return[User(user_id,first_name,last_name)for(user_id,first_name,last_name)indbrows]请注意,第二个版本更易于阅读。在函数的第一个版本(bad_users_from_rows)中,数据以row[0]、row[1]和row[2]的形式存在,但并未说明它们是什么。换句话说,user_id、first_name和last_name等变量代表它们自己。我们可以在设计功能时利用这种能力。我们可以在标准库中找到一个例子是max函数,它的定义如下:max(...)max(iterable,*[,default=obj,key=func])->valuemax(arg1,arg2,*args,*[,key=func])->value使用单个可迭代参数,返回其最大项。如果提供的可迭代对象为空,则默认的仅关键字参数指定要返回的对象。有两个或多个参数,返回最大的参数。有一个类似的符号,其中关键字参数由两个星号(**)表示。如果我们有一个字典,我们将它传递给一个带有两个星号的函数,该函数将选择键作为参数的名称,然后将键的值作为函数中该参数的值传递。例如,下面这行代码:function(**{"key":"value"})等同于:function(key="value")相反,如果我们定义一个参数以两个星号开头的函数,则相反的情况——键提供的参数将被打包到字典中:>>>deffunction(**kwargs):...print(kwargs)...>>>function(key="value"){'key':'value'}函数中的参数个数我们认为,如果一个函数或方法中的参数过多,则意味着代码设计不好(“代码味道”)。鉴于此,我们将给出解决这个问题的方法。一种解决方案是更通用的软件设计原则——具体化(为所有传递的参数创建一个新对象,这可能是我们缺少的抽象)。将多个参数压缩到一个新对象中并不是Python特有的解决方案,但可以应用于任何编程语言。另一种解决方案是使用我们在上一节中看到的特定于Python的功能,以利用可变位置参数和关键字参数来创建具有动态签名的函数。虽然这可能是一种Pythonic的做事方式,但我们必须小心不要滥用此功能,因为它可能会创建过于动态而无法维护的东西。在这种情况下,我们应该看看函数体。不管签名或参数看起来是否正确,如果函数使用参数的值做了太多不同的事情,那么这就是一个迹象——它必须被分解成多个更小的函数。(记住,一个函数应该做一件事,而且只能做一件事!)1.函数参数和耦合函数签名的参数越多,该参数就越有可能与调用函数紧密耦合。假设有两个函数f1和f2,函数f2有5个参数。f2收到的参数越多,任何试图调用该函数来收集所有信息并将其向下传递以使其工作的人就越难。现在,f1似乎拥有所有这些信息,因为它正确地调用了f1,从中我们可以得出两个结论。首先,f2可能是一个有漏洞的抽象,这意味着当f1知道f2需要的一切时,它几乎可以知道自己在做什么并且能够自己完成。总而言之,f2并没有抽象那么多。其次,f2似乎只对f1有用,很难想象在不同的??上下文中使用这个函数,这使得重用更加困难。当函数具有更通用的接口并且能够处理更高级别的抽象时,它们将变得更可重用。这适用于所有类型的函数和对象方法,包括类的__init__方法。此方法的存在通常(但不总是)意味着应该传递一个新的更高级别的抽象,或者缺少一个对象。如果一个函数需要太多参数才能正常工作,它可以被认为是“代码味道”。其实这是一个设计问题——静态分析工具,比如Pylint(见第一章),遇到这种情况默认会发出警告。如果发生这种情况,不要抑制警告,而是重构它。2.清理接受过多参数的函数签名假设我们发现一个接受过多参数的函数,并且知道我们不能将它原封不动地放在代码库中并且必须重构它。但是以什么方式呢?根据情况,我们可以应用以下一些规则。虽然这些规则的适用范围并不广泛,但它们可以为我们提供解决这些常见问题的思路。如果您发现大多数参数属于一个公共对象,有时可以通过一种简单的方法来更改参数。例如,考虑这样一个函数调用:track_request(request.headers,request.ip_addr,request.request_id)现在,该函数可能会或可能不会接收其他参数,但这是显而易见的:所有参数都取决于请求,所以为什么不呢直接传递请求对象怎么样?这是一个简单的更改,但它显着改进了代码。正确的函数调用应该是track_request(request)方法。此外,从语义上讲,调用track_request(request)方法也更有意义。虽然鼓励传递此类参数,但在将可变对象传递给函数的所有情况下,我们必须非常小心副作用。我们调用的函数不应该对传递的对象进行任何修改,因为这会改变对象,产生不需要的副作用。除非这实际上是期望的效果(在这种情况下必须明确说明),否则不鼓励这种行为。即使当我们真的想改变我们正在处理的对象上的某些东西时,更好的选择是复制它并返回(新的)修改后的版本。处理不可变对象并尽可能避免副作用。这给我们带来了一个类似的主题:分组参数。在前面的示例中,我们对参数进行了分组,但没有使用组(在本例中为请求对象)。但是其他情况没有这种情况明显,我们可能希望将参数中的所有数据分组到一个可以充当容器的对象中。不用说,这种分组一定是有意义的。这里的想法是具体化:创建设计中缺失的抽象。如果之前的策略不起作用,作为最后的手段,我们可以更改函数的签名以接受可变数量的参数。如果参数数量太多,使用args或*kwargs会使事情变得更加混乱,因此我们必须确保正确记录和使用接口,但在某些情况下值得这样做。的确,使用args和*kwargs定义的函数非常灵活和适应性强,但缺点是失去了它的签名,以及它的部分含义和几乎所有的易读性。我们已经看到了变量名(包括函数参数)如何使代码更易于阅读的示例。如果一个函数要接受任意数量的参数(位置或关键字),当我们想看看这个函数将来能做什么时,我们可能无法通过这些参数知道这一点,除非有一个真正的好的文档。以上就是本次分享的全部内容。现在想学习编程的朋友欢迎关注Python技术大本营获取更多技能和教程。