authors are vetted experts in their fields and write on topics in which they have demonstrated experience. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
卢克·普兰特的头像

Luke Plant

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

Expertise

工作经验

21

Share

In this post, I’m going to talk about what I consider to be the most important technique or pattern in producing clean, 神谕的代号, parameterization. 这篇文章适合你,如果:

  • You are relatively new to the whole design patterns thing and perhaps a bit bewildered by long lists of pattern names and class diagrams. The good news is there is really only one design pattern that you absolutely must know for Python. 更好的是,你可能已经知道它了,但也许不是所有的应用方式.
  • You have come to Python from another OOP language such as Java or C# and want to know how to translate your knowledge of design patterns from that language into Python. 在Python和其他动态类型语言中, 静态类型OOP语言中常见的许多模式是“不可见的”或更简单的,正如作家彼得·诺维格所说的那样.

In this article, we’ll explore the application of “parameterization” and how it can relate to mainstream design patterns known as 依赖注入, strategy, template method, abstract factory, factory method, and decorator. In Python, many of these turn out to be simple or are made unnecessary by the fact that parameters in Python can be callable objects or classes.

Parameterization is the process of taking values or objects defined within a function or a method, 让它们成为那个函数或方法的参数, 以便泛化代码. 这个过程也被称为“提取参数”重构. 在某种程度上,本文是关于设计模式和重构的.

Python参数化的最简单例子

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

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

从海龟导入海龟

turtle = Turtle()

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

假设我们现在想要画一个不同大小的正方形. A very junior programmer at this point would be tempted to copy-paste this block and modify. 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, there is a large range of things that can become parameters—more than in some other languages—and this makes it a very powerful technique.

任何是对象的东西

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, we later discover that sometimes we actually want to do something completely different at the corners. 也许我们想在每个角上画另一个形状,改变笔的颜色,等等. 我们可能想要添加更多的参数,每个参数对应我们需要做的事情. However, a much nicer solution would be to allow any function to be passed in as the action to take. 作为默认值,我们将创建一个什么都不做的函数. 我们还将使该函数接受本地的 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)

Or, we could do something a bit cooler like recursively draw smaller squares at each corner:

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)

Illustration of recursively drawn smaller squares as demonstrated in python parameterized code above

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

在其他语言中…

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

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

类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 方法,完成我们需要的. 这种蟒蛇模式被称为 模板方法模式—a base class defines the shape of the whole operation or algorithm and the variant portions of the operation are put into methods that need to be implemented by subclasses.

虽然这在Python中有时可能很有帮助, pulling out the variant code into a function that is simply passed as a parameter is often going to be much simpler.

A second way we might approach this problem in languages without first class functions is to wrap our functions up as methods inside classes, 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

In the examples above, I’ve talked about passing functions into other functions as parameters. 然而,实际上,我所写的所有内容都适用于任何可调用对象. 函数是最简单的例子,但我们也可以考虑方法.

假设我们有一个列表 foo:

foo = [1, 2, 3]

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

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

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

类作为参数

In Python, classes are “first class”—they are run-time objects just like dicts, strings, etc. 这可能看起来比函数是对象更奇怪, but thankfully, 实际上,证明这个事实比证明函数更容易.

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

class Foo:
    pass

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

在第二个版本中, note the two things we just did (which are done more conveniently using the class statement):

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

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

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

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

类作为工厂

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

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

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

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

回到上面的海龟例子, one problem with using turtles for drawing is that the position and orientation of the drawing depend on the current position and orientation of the turtle, 它也可以让它处于不同的状态,这可能对调用者没有帮助. To solve this, our draw_square function could create its own turtle, move it to the desired position, and then draw a 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)

但是,我们现在有一个定制问题. Suppose the caller wanted to set some attributes of the turtle or use a different kind of turtle that has the same interface but has some special behavior?

我们可以通过依赖注入来解决这个问题, 如前所述,调用者将负责设置 Turtle object. But what if our function sometimes needs to make many turtles for different drawing purposes, 或者如果它想启动四个线程, 每个人都用自己的乌龟画正方形的一边? 答案很简单,将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 (which requires the creation of a set of classes whose only job is to instantiate other classes) and the 工厂方法模式. As you can see, in Python, it is just a matter of pulling out the class as a parameter because a class is its own factory.

类作为基类

假设我们发现自己创建了子类,将相同的特性添加到不同的类中. 例如,我们想要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. The name of the sub-class—but we don’t really care about that and could generate it automatically from the base class __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__ 这是完全可选的,但可以帮助调试.

Another application of this technique is when we have a whole bunch of features that we sometimes want to add to a class, 我们可能想要添加这些特征的各种组合. 手动创建我们需要的所有不同组合可能会变得非常笨拙.

In languages where classes are created at compile-time rather than run-time, this isn’t possible. 相反,你必须使用 decorator pattern. That pattern may be useful sometimes in Python, but mostly you can just use the technique above.

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

类作为例外

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

For example, the following code implements a very generic strategy of attempting an action that could fail and retrying with exponential backoff until a maximum number of attempts is reached:

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, 只对某些参数组合进行适当的测试.
  • Unhelpful coupling of different parts of the codebase because their “common code” has been factored out into a single place. 有时两个地方的代码只是偶然相似, 这两个地方应该是相互独立的,因为 它们可能需要独立地改变.

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

在这篇文章中,我们介绍了被称为 依赖注入, strategy, template method, abstract factory, factory method, and decorator. In Python, many of these really do turn out to be a simple application of parameterization or are definitely made unnecessary by the fact that parameters in Python can be callable objects or classes. Hopefully, this helps to lighten the conceptual load of “things you are supposed to know as a real 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中的形参和实参是什么?

    Parameters are the variables that appear between the brackets in the “def” line of a Python function definition. 参数是在调用函数或方法时传递给它的实际对象或值. 这些术语通常可以互换使用.

就这一主题咨询作者或专家.
Schedule a call
卢克·普兰特的头像
Luke Plant

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

Member since July 18, 2017

About the author

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

Toptalauthors are vetted experts in their fields and write on topics in which they have demonstrated experience. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.

Expertise

工作经验

21

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

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

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

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

Toptal Developers

Join the Toptal® community.