作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
卢克·普兰特的头像

Luke Plant

自2006年以来一直是Django核心开发人员, Luke是一名全栈开发人员,主要使用Python,专注于服务器端技术.

Expertise

Years of Experience

21

Share

In this post, 我要讲的是我认为最重要的生产清洁的技术或模式, 神谕的代号, parameterization. 这篇文章适合你,如果:

  • 对于整个设计模式,您相对来说还是个新手,可能会对一长串的模式名称和类图感到困惑. 好消息是,对于Python,实际上只有一种设计模式是您绝对必须知道的. 更好的是,你可能已经知道它了,但也许不是所有的应用方式.
  • 您是从另一种面向对象语言(如Java或c#)转到Python的,并且想知道如何将您的设计模式知识从该语言转换到Python中. 在Python和其他动态类型语言中, 静态类型OOP语言中常见的许多模式是“不可见的”或更简单的,正如作家彼得·诺维格所说的那样.

In this article, 我们将探讨“参数化”的应用,以及它是如何与主流设计模式相关联的 依赖注入, strategy, template method, abstract factory, factory method, and decorator. In Python, 由于Python中的参数可以是可调用的对象或类,因此其中许多都很简单,或者没有必要.

参数化是获取函数或方法中定义的值或对象的过程, 让它们成为那个函数或方法的参数, 以便泛化代码. 这个过程也被称为“提取参数”重构. 在某种程度上,本文是关于设计模式和重构的.

Python参数化的最简单例子

对于我们的大多数示例,我们将使用指导性标准库 turtle 做一些图形的模块.

下面是一些代码,将绘制一个100x100的正方形使用 turtle:

从海龟导入海龟

turtle = Turtle()

对于I在(0,4)范围内:
    turtle.forward(100)
    turtle.left(90)

假设我们现在想要画一个不同大小的正方形. 此时,一个非常初级的程序员可能会试图复制粘贴这个块并进行修改. Obviously, 更好的方法是首先将正方形绘制代码提取到函数中, 然后将正方形的大小作为这个函数的一个参数:

def draw_square(尺寸):
    对于I在(0,4)范围内:
        turtle.forward(size)
        turtle.left(90)

draw_square(100)

现在我们可以画任意大小的正方形 draw_square. 这就是参数化的基本技术, 我们刚刚看到了第一个主要的消除使用的复制粘贴编程.

上面代码的一个直接问题是 draw_square 取决于一个全局变量. This has 很多不好的后果有两种简单的方法可以解决这个问题. 第一个是 draw_square to create the Turtle 实例本身(稍后讨论). 如果我们想要使用单个的,这可能是不可取的 Turtle 为了我们所有的画. 现在,我们将再次简单地使用参数化 turtle a parameter to draw_square:

从海龟导入海龟

Def draw_square(turtle, size):
    对于I在(0,4)范围内:
        turtle.forward(size)
        turtle.left(90)

turtle = Turtle()
draw_square(龟,100)

它有一个奇特的名称依赖注入. 它的意思是如果一个函数需要某种对象来完成它的工作,比如 draw_square needs a Turtle,调用方负责将该对象作为参数传入. 不,真的,如果您曾经对Python依赖注入感到好奇,这就是它.

到目前为止,我们已经处理了两个非常基本的用法. 本文其余部分的关键观察是, in Python, 与其他语言相比,有很多东西可以成为参数,这使得它成为一种非常强大的技术.

任何是对象的东西

In Python, 您可以使用这种技术对任何对象进行参数化, and in Python, 你遇到的大多数事情都是这样, in fact, objects. This includes:

  • 内置类型的实例,如字符串 "I'm a string" and the integer 42 or a dictionary
  • 其他类型和类的实例,例如.g., a datetime.datetime object
  • 函数和方法
  • 内置类型和自定义类

最后两个是最令人惊讶的, 特别是如果你来自其他语言, 他们需要更多的讨论.

函数作为参数

Python中的function语句做两件事:

  1. 它创建一个函数对象.
  2. 它在指向该对象的局部作用域中创建一个名称.

我们可以在REPL中使用这些对象:

> >> def foo():
...     返回"Hello from foo"
> >>
> >> foo()
'Hello from foo'
> >> print(foo)

> >> type(foo)

> >> foo.name
'foo'

就像所有对象一样,我们可以将函数赋值给其他变量:

> >> bar = foo
> >> bar()
'Hello from foo'

Note that bar 是同一对象的另一个名字,所以它有相同的内部 __name__ 财产:

> >> bar.name
'foo'
> >> bar

但关键的一点是,因为函数只是对象, 在任何你看到一个函数被使用的地方, 它可以是一个参数.

So, 假设我们扩展上面的正方形绘制函数, 现在,有时当我们画正方形时,我们想在每个角暂停——调用 time.sleep().

但假设有时我们不想暂停. 最简单的方法是添加a pause 参数,可能默认为0,因此默认情况下我们不会暂停.

However, 后来我们发现,有时候我们真的想在角落里做一些完全不同的事情. 也许我们想在每个角上画另一个形状,改变笔的颜色,等等. 我们可能想要添加更多的参数,每个参数对应我们需要做的事情. 然而,一个更好的解决方案是允许将任何函数作为要执行的操作传入. 作为默认值,我们将创建一个什么都不做的函数. 我们还将使该函数接受本地的 turtle and size 参数,如果需要的话:

Def do_nothing(turtle, size):
    pass

Def draw_square(turtle, size, at_corner=do_nothing):
    对于I在(0,4)范围内:
        turtle.forward(size)
        at_corner(龟、大小)
        turtle.left(90)

Def pause(turtle, size):
    time.sleep(5)

turtle = Turtle()
Draw_square (turtle, 100, at_corner=pause)

或者,我们可以做一些更酷的事情,比如递归地在每个角上画更小的正方形:

Def smaller_square(turtle, size):
    if size < 10:
        return
    Draw_square (turtle, size / 2, at_corner=smaller_square)

Draw_square (turtle, 128, at_corner=smaller_square)

上面python参数化代码中所演示的递归绘制较小正方形的说明

当然,这种说法也有不同的变体. 在许多示例中,将使用函数的返回值. Here, 我们有更命令式的编程风格, 这个函数只因为它的副作用而被调用.

在其他语言中…

在Python中拥有第一类函数使这变得非常容易. 在没有它们的语言中, 或者一些需要参数类型签名的静态类型语言, this can be harder. 如果没有第一类函数,我们怎么做呢?

一个解决办法是转向 draw_square into a class, SquareDrawer:

class SquareDrawer:
    Def __init__(self, size):
        self.size = size

    def draw(self, t):
        对于I在(0,4)范围内:
            t.forward(self.size)
            self.at_corner(t, size)
            t.left(90)

    defat_corner (self, t, size):
        pass

现在我们可以创建子类 SquareDrawer and add an at_corner 方法,完成我们需要的. 这种蟒蛇模式被称为 模板方法模式-基类定义整个操作或算法的形状,操作的可变部分放入需要由子类实现的方法中.

虽然这在Python中有时可能很有帮助, 将变体代码提取到简单地作为参数传递的函数中通常会简单得多.

在没有第一类函数的语言中,我们可能解决这个问题的第二种方法是将函数包装为类中的方法, like this:

 class DoNothing:
     Def run(self, turtle, size):
         pass


def draw_square(turtle, size, at_corner=DoNothing()):
     对于I在(0,4)范围内:
         turtle.forward(size)
         at_corner.run(turtle, size)
         t.left(90)


 class Pauser:
     Def run(self, turtle, size):
         time.sleep(5)

 draw_square(turtle, 100, at_corner=Pauser())

这被称为 strategy pattern. Again, 这当然是一个在Python中使用的有效模式, 特别是当策略类实际上包含一组相关函数时, 而不仅仅是一个. 然而,通常我们真正需要的是一个函数,我们可以 停止写类.

Other Callables

在上面的例子中,我讨论了如何将函数作为参数传递给其他函数. 然而,实际上,我所写的所有内容都适用于任何可调用对象. 函数是最简单的例子,但我们也可以考虑方法.

假设我们有一个列表 foo:

foo = [1, 2, 3]

foo 现在有一大堆附加的方法,比如 .append() and .count(). 这些“绑定方法”可以像函数一样传递和使用:

> >> appendtofoo = foo.append
> >> appendtofoo(4)
> >> foo
[1, 2, 3, 4]

除了这些实例方法之外,还有其他类型的可调用对象staticmethods and classmethods类的实例 __call__,以及类/类型本身.

类作为参数

在Python中,类是“第一类”——它们是运行时对象,就像字典、字符串等一样. 这可能看起来比函数是对象更奇怪, but thankfully, 实际上,证明这个事实比证明函数更容易.

您熟悉的class语句是创建类的好方法, 但这不是唯一的方法,我们也可以使用 类型的三个参数版本. 下面两个语句的作用完全相同:

class Foo:
    pass

Foo = type('Foo', (), {})

在第二个版本中, 注意我们刚刚做的两件事(使用class语句更方便):

  1. 在等号的右侧,我们创建了一个新类,其内部名称为 Foo. 如果你这么做,你就会得到这个名字 Foo.__name__.
  2. 有了作业, 然后,我们在当前作用域中创建了一个名称, Foo, 哪个指向我们刚刚创建的类对象.

我们对函数语句做了同样的观察.

这里的关键观点是,类是可以分配名称的对象.e.,可以放在变量中). 当你看到一个类在被使用时,你实际上只是看到了一个被使用的变量. 如果它是一个变量,它可以是一个参数.

我们可以把它分成几种用法:

类作为工厂

类是创建自身实例的可调用对象:

> >> class Foo:
...    pass
> >> Foo()
<__main__.Foo at 0x7f73e0c96780>

作为一个对象,它可以被赋值给其他变量:

> >> myclass = Foo
> >> myclass()
<__main__.Foo at 0x7f73e0ca93c8>

回到上面的海龟例子, 使用海龟进行绘图的一个问题是,绘图的位置和方向取决于海龟的当前位置和方向, 它也可以让它处于不同的状态,这可能对调用者没有帮助. To solve this, our draw_square 函数可以创建自己的海龟,将其移动到所需的位置,然后绘制一个正方形:

Def draw_square(x, y, size):
    turtle = Turtle()
    turtle.penup() #移动到起始位置时不绘图
    turtle.goto(x, y)
    turtle.pendown()
    对于I在(0,4)范围内:
        turtle.forward(size)
        turtle.left(90)

但是,我们现在有一个定制问题. 假设调用者想要设置海龟的某些属性,或者使用具有相同接口但具有某些特殊行为的另一种海龟?

我们可以通过依赖注入来解决这个问题, 如前所述,调用者将负责设置 Turtle object. 但是,如果我们的函数有时需要为不同的绘图目的生成许多海龟,该怎么办呢, 或者如果它想启动四个线程, 每个人都用自己的乌龟画正方形的一边? 答案很简单,将Turtle类作为函数的参数. 我们可以使用带有默认值的关键字参数, 为了让不关心的调用者保持简单:

def draw_square(x, y, size, make_turtle=Turtle):
    Turtle = make_turtle()
    turtle.penup()
    turtle.goto(x, y)
    turtle.pendown()
    对于I在(0,4)范围内:
        turtle.forward(size)
        turtle.left(90)

为了使用它,我们可以写a make_turtle 创建并修改海龟的函数. 假设我们想在绘制正方形时隐藏乌龟:

def make_hidden_turtle ():
    turtle = Turtle()
    turtle.hideturtle()
    return turtle

Draw_square (5,10,20, make_turtle=make_hidden_turtle)

或者我们可以创建子类 Turtle 要使该行为内建并将子类作为参数传递:

类HiddenTurtle(乌龟):
    Def __init__(self, *args, **kwargs):
        super().__init__ (* args, * * kwargs)
        self.hideturtle()

draw_square(5,10,20, make_turtle=HiddenTurtle)

在其他语言中…

其他一些OOP语言,如Java和c#,缺乏第一类类. 要实例化一个类,必须使用 new 关键字后面跟着实际的类名.

这种限制是出现如下模式的原因 abstract factory (这需要创建一组类,这些类的唯一任务是实例化其他类)和 工厂方法模式. As you can see, in Python, 这只是将类作为参数拉出来的问题,因为类是它自己的工厂.

类作为基类

假设我们发现自己创建了子类,将相同的特性添加到不同的类中. 例如,我们想要a Turtle 创建时将写入日志的子类:

import logging
logger = logging.getLogger()

类LoggingTurtle(乌龟):
    Def __init__(self, *args, **kwargs):
        super().__init__ (* args, * * kwargs)
        logger.调试("Turtle被创建")

但随后,我们发现自己对另一个类做了完全相同的事情:

类LoggingHippo(河马):
    Def __init__(self, *args, **kwargs):
        super().__init__ (* args, * * kwargs)
        logger.调试(“创建了河马”)

这两者之间唯一不同的是:

  1. The base class
  2. 子类的名称——但我们并不关心这个,我们可以从基类自动生成它 __name__ attribute.
  3. 中使用的名称 debug 调用——同样,我们可以从基类名生成这个.

面对两个非常相似的代码,只有一个变体,我们该怎么办? 就像第一个例子一样, 我们创建一个函数,并将变量部分作为参数提取出来:

def make_logging_class (cls):

    类LoggingThing (cls):
        Def __init__(self, *args, **kwargs):
            super().__init__ (* args, * * kwargs)
            logger.调试("{0}已创建").format(cls.__name__))

    LoggingThing.__name__ = "日志{0}".format(cls.__name__)
    返回LoggingThing

LoggingTurtle = make_logging_class(Turtle)
LoggingHippo = make_logging_class(Hippo)

在这里,我们有一个一流课程的示范:

  • 我们将一个类传递给一个函数,并为参数指定一个常规名称 cls 避免与关键词冲突 class (you will also see class_ and klass 用于此目的).
  • 在函数内部,我们创建了一个类——注意,对这个函数的每次调用都会创建一个 new class.
  • 我们将这个类作为函数的返回值返回.

We also set LoggingThing.__name__ 这是完全可选的,但可以帮助调试.

这种技术的另一个应用是当我们有时想要向一个类添加一大堆特性时, 我们可能想要添加这些特征的各种组合. 手动创建我们需要的所有不同组合可能会变得非常笨拙.

在类是在编译时而不是运行时创建的语言中,这是不可能的. 相反,你必须使用 decorator pattern. 这种模式在Python中有时可能有用,但大多数情况下您可以使用上面的技术.

通常,我实际上会避免创建大量用于自定义的子类. 通常,有一些更简单、更python化的方法根本不涉及类. 但是如果你需要的话,这个技巧是可用的. See also Brandon Rhodes对Python中装饰器模式的全面介绍.

类作为例外

类被使用的另一个地方是 except try/except/finally语句的子句. 毫无疑问,我们也可以对这些类进行参数化.

For example, 下面的代码实现了一个非常通用的策略,尝试一个可能失败的动作,然后用指数回退重新尝试,直到达到最大尝试次数:

import time

(动作,exceptions_to_catch,
                       max_attempts = 10, attempts_so_far = 0):
    try:
        return action()
    除了exceptions_to_catch:
        Attempts_so_far += 1
        if attempts_so_far >= max_attempts:
            raise
        else:
            time.Sleep (attempts_so_far ** 2)
            返回retry_with_backoff(action, exceptions_to_catch,
                                      attempts_so_far = attempts_so_far,
                                      max_attempts = max_attempts)

我们提取了要采取的操作和要捕获的异常作为参数. The parameter exceptions_to_catch 可以是单个类,比如 IOError or httplib.client.HTTPConnectionError,或者由这样的类组成的元组. (我们要避免使用" bare except "从句或even except Exception because 众所周知,这可以隐藏其他编程错误).

警告和结论

参数化是一种用于重用代码和减少代码重复的强大技术. 它并非没有一些缺点. 在追求代码重用的过程中,经常会出现以下几个问题:

  • 过于泛型或抽象的代码变得非常难以理解.
  • Code with a 参数扩散 这会模糊大局或引入bug,因为, in reality, 只对某些参数组合进行适当的测试.
  • 代码库不同部分的无用耦合,因为它们的“公共代码”被分解到一个地方. 有时两个地方的代码只是偶然相似, 这两个地方应该是相互独立的,因为 它们可能需要独立地改变.

有时候,少量的“重复”代码比这些问题要好得多, 所以要小心使用这个技巧.

在这篇文章中,我们介绍了被称为 依赖注入, strategy, template method, abstract factory, factory method, and decorator. In Python, 其中许多实际上是参数化的简单应用程序,或者由于Python中的参数可以是可调用的对象或类而完全没有必要. 希望这有助于减轻“你应该知道的真实事物”的概念负担 Python developer,使您能够编写简洁的python代码!

了解基本知识

  • 什么是参数化函数?

    在参数化函数中, one or more of the details of what the function does are defined as parameters instead of being defined in the function; they have to be passed in by the calling code.

  • 重构代码的目的是什么?

    当你重构代码时, 例如,您可以更改它的形状,以便更容易重用或修改它, 修复错误或添加功能.

  • 我可以在Python中传递函数作为参数吗?

    是的——函数在Python和Python中是对象, like all objects, 可以作为参数传递到函数和方法. 不需要特殊的语法.

  • Python中的形参和实参是什么?

    参数是出现在Python函数定义的" def "行括号之间的变量. 参数是在调用函数或方法时传递给它的实际对象或值. 这些术语通常可以互换使用.

聘请Toptal这方面的专家.
Hire Now
卢克·普兰特的头像
Luke Plant

Located in 土耳其开塞利省开塞利

Member since July 18, 2017

About the author

自2006年以来一直是Django核心开发人员, Luke是一名全栈开发人员,主要使用Python,专注于服务器端技术.

Toptal作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.

Expertise

Years of Experience

21

世界级的文章,每周发一次.

订阅意味着同意我们的 privacy policy

世界级的文章,每周发一次.

订阅意味着同意我们的 privacy policy

Toptal Developers

Join the Toptal® community.