Python 从 Java 程序员的角度看 OOP

Python OOP from a Java programmer's perspective

我只有 Java 的 OOP 编程经验,并且刚刚开始在 Python 从事一个项目,我开始意识到 Python 让我对一些事情持怀疑态度这对我来说很直观。所以我在 Python.

中有几个关于 OOP 的问题

场景:我正在编写一个发送电子邮件的程序。对于电子邮件,tofromtextsubject 字段将是必需的,其他字段如 ccbcc 是可选的.此外,将有一堆 classes 将实现核心邮件功能,因此它们将派生自基础 class (Mailer).

以下是我不完整的代码片段:

class Mailer(object):
    __metaclass__ == abc.ABCMeta

    def __init__(self,key):
        self.key = key

    @abc.abstractmethod
    def send_email(self, mailReq):
        pass


class MailGunMailer(Mailer):
    def __init__(self,key):
        super(MailGunMailer, self).__init__(key)

    def send_email(self, mailReq):
        from = mailReq.from
        to = mailReq.to
        subject= mailReq.subject
        text = mailReq.text
        options = getattr(mailReq,'options',None)
        if(options != None):
            if MailRequestOptions.BCC in options:
                #use this property
                pass
            if MailRequestOptions.CC in options:
                #use this property
                pass



class MailRequest():
    def __init__(self,from,to,subject,text):
        self.from = from
        self.to = to
        self.subject = subject
        self.text = text

    def set_options(self,options):
        self.options = options

class MailRequestOptions():
    BCC = "bcc"
    CC = "cc"

问题:

  1. send_mail 方法可以采用多个参数(from, to, subject, text, cc, bcc 等),而我的应用程序只需要其中四个参数。由于该方法的参数数量太多,我决定创建一个名为 MailRequest 的包装对象,它将四个必要的参数作为属性,所有其他参数都可以在 options 中定义字典。问题是,在这里,只看代码就没有办法说 options 是。是 dict 还是 list?此外,查看 send_email 方法,也无法判断 mailReq 是什么。 这是糟糕的编程习惯吗?我应该做点别的吗?来自 Java 的世界,写代码时看代码无法判断参数是什么,这让我很不自在。我在 Python 中了解了注释,但我不想使用它们,因为它们仅在更高版本中受支持。

  2. 由于 options dict 应该用于指定许多其他属性(ccbcc 只是其中的两个),我创建了一个新的 class 称为 MailRequestOptions,所有选项都可以在选项字典中指定为 MailRequestOptions 的静态字符串。 这也是不好的做法吗,或者有更好的方法吗?我知道这不是 Python 具体的。

Python 是一种 "duck-typed" 语言;如果它走路像鸭子,叫起来像鸭子,那它就是鸭子!或者,在实现方面,如果作为 mailReq 传递的对象具有 fromtosubjecttext 属性, 它不会是不是 MailRequest 真的很重要

如果你想记录界面(这当然是个好主意),通常使用 docstrings to do so. I like the Google style,它可以与 sphinx-napoleon 一起使用来自动生成人类可读的文档, 但其他可用。

def send_email(self, mailReq):
    """Send an email.

    Args:
      mailReq (MailRequest): the configuration for the email.

    """
    ...

关于你的第二个问题;将大量参数包装到容器对象中是 pretty common pattern. In Python, you have the option of making things a bit simpler using "**kwargs magic" (see e.g. What does ** (double star) and * (star) do for parameters?):

def send_email(self, from_, to, subject, text, **config):
    ...
    bcc = config.get('bcc', [])  # option from config or a default
    ...

(注意from是Python中的关键字,不能作为参数名)

这具有合理的自我记录的优势 - 有四个必需的参数,加上一些任意的附加关键字配置选项(同样,通常会在文档字符串中记录)。

  1. 在Python中不需要专门创建另一个对象。如果要包装邮件请求,可以使用字典:mailReq = {'from': 'johnsmith@british.com', 'to': '....', ...}

  2. 您应该尝试使用 *args**kwargs 作为方法。它可以使选项更简单:def send_mail(from, to, subject, text, **kwargs),然后可以使用例如检索其他选项kwargs['bcc']。我相信这样会更 Pythonic.

虽然 jonrsharpe 提供了一个很好的解决方案,但我认为值得一提的是我的方法。

如前所述,在动态语言中您不关心类型,只关心对象具有的接口(所谓的 "duck")。然后,您的 MailRequest 对象是将逻辑上属于一起的参数分组的绝佳方式。但是,它并没有实现它应该实现的一切。这将是我的方法:

class MailRequest(object):
    def __init__(self, from_, to, subject, text, bcc=None, cc=None):
        # I am asuming a good default for bbc is an empty list. If none
        # is fine, just remove the None checks.
        # Dont get confused about this, it just avoids a pitfall with
        # mutable default arguments. There are other techniques however

        if bcc is None:
            bcc = []

        if cc is None:
            cc = []

        self.from_ = from_
        self.to = to
        self.subject = subject
        self.text = text
        self.bcc = bcc
        self.cc = cc

    # No options needed

然后 send_email 函数将如下所示:

def send_email(self, mailReq):
    """
    :type mailReq: MailRequest

    """
    from_ = mailReq.from_
    to = mailReq.to
    subject= mailReq.subject
    text = mailReq.text
    bcc = mailReq.bcc
    cc = mailReq.cc

请注意,您只是记录了 mailReq 参数,指出传递的任何对象都应提供 MailRequest 接口(至少部分)。这样,您将参数文档委托给 MailRequest class.

我更喜欢这种方法而不是 **kwargs 魔法,因为参数在某些时候被显式传递给严格的签名,它以某种方式用作文档。缺点是冗长。

编辑

如果您担心 MailRequest "constructor" 中的争论激增,解决方案是更深入地执行相同的操作:再次分组。例如,您可能希望将选项分组到它自己的 class:

class MailRequestOpts(object):

    def __init__(self, bbc=None, cc=None, colour=None, lights='blue', blink=True):
        # ...
        self.bbc = bbc
        self.cc = cc 
        self.colour = colour
        # etc...

然后 MailRequestClass 看起来像这样:

class MailRequest(object):
    def __init__(self, from_, to, subject, text, options=None):
        """
        :type options: MailRequestOpts

        """
        if options is None:
            options = MailRequestOpts()

        # ...
        self.options = options

最后,如果一个进程需要 50 个参数,则无法避免在某个时候将它们全部传递给一个或多个分布式函数。将它们分组多少次取决于您以及您在哪里找到平衡点。