【问题标题】:How to design code in Python? [closed]如何在 Python 中设计代码? [关闭]
【发布时间】:2018-05-26 04:19:30
【问题描述】:

我来自 Java 并学习 Python。到目前为止,我发现非常酷但很难适应的是不需要声明类型。我知道每个变量都是一个指向对象的指针,但到目前为止我还无法理解如何设计我的代码。

例如,我正在编写一个接受 2D NumPy 数组的函数。然后在函数体中,我调用了这个数组的不同方法(这是 Numpy 中array 的对象)。但是将来假设我想使用这个函数,到那时我可能已经完全忘记了我应该将什么作为类型传递给函数。人们通常会做什么?他们只是为此编写文档吗?因为如果是这样的话,那么这涉及更多的类型,并且会提出关于不声明类型的想法的问题。

另外假设我想在将来传递一个类似于数组的对象。通常在 Java 中,一个人会实现一个接口,然后让两个类来实现这些方法。然后在函数参数中,我将变量定义为接口的类型。如何在 Python 中解决这个问题,或者可以使用哪些方法来实现相同的想法?

【问题讨论】:

  • 鸭式打字的目的不是一开始就少写代码
  • 如果你的代码由于某种原因确实依赖于传入的特定类型,你可以使用assert isinstance(foo, Foo)作为函数/方法的第一行(这也可以作为阅读文档时的文档代码),但这通常只是限制了以后可以用该函数做什么。
  • @ErikAllik:不要断言,引发 TypeError
  • @NeilG:嗯,是的,是的;取决于你;有时您希望assertions 确保您正确理解了自己的代码,但当然这比参数类型更通用。

标签: python


【解决方案1】:

这是一个非常健康的问题。

鸭子打字

首先要了解python是duck typing的概念:

如果它走路像鸭子,叫起来像鸭子,那我就叫它鸭子

与 Java 不同,Python 的类型从不显式声明。无论是在编译时还是在运行时,对象可以采用的类型都没有限制。

您所做的只是将对象视为满足您的需求的完美类型。你不会询问或怀疑它的类型。如果它实现了您希望它具有的方法和属性,那么就是这样。会的。

def foo(duck):
    duck.walk()
    duck.quack()

这个函数唯一的契约是duck暴露了walk()quack()。一个更精致的例子:

def foo(sequence):
    for item in sequence:
        print item

sequence 是什么? list?一个麻木的arraydict? generator?没关系。如果它是可迭代的(也就是说,它可以在 for ... in 中使用),那么它就可以达到它的目的。

类型提示

当然,没有人可以一直担心对象类型错误。这可以通过编码风格、约定和良好的文档来解决。例如:

  • 名为count 的变量应该包含一个整数
  • 以大写字母开头的变量Foo 应包含type (class)
  • 默认值为False 的参数bar 在被覆盖时也应包含bool

请注意,鸭子类型的概念可以应用于以下 3 个示例:

  • count 可以是实现+-< 的任何对象
  • Foo 可以是任何返回对象实例的可调用对象
  • bar 可以是任何实现 __nonzero__ 的对象

换句话说,类型从未明确定义,但总是强烈暗示。或者更确切地说,对象的功能总是被暗示,它的确切类型并不相关。

使用未知类型的对象是很常见的。大多数框架公开的类型看起来像列表和字典,但实际上并非如此。

最后,如果你真的需要知道,这里有文档。您会发现 Python 文档远远优于 Java。永远值得一读。

【讨论】:

  • 这是一个非常健康的答案。
  • 健康问题首先意味着什么? :p
  • @AlexTwain 在我看来:“非常有趣,很好问。这不是关于语法错误的愚蠢问题,而是每个 Python 程序员都应该知道的关于 Python 哲学的真正问题”
  • “一个名为count的变量应该是一个整数”——或者一个欧洲贵族。我当然是开玩笑的,但实际上,在“自我记录”代码中确实需要小心谨慎,以免出现真正的歧义或不精确的名称。
  • 有时鸭子打字不起作用。如果您的函数需要一个字符串序列并且您将其传递给一个字符串,那么您的函数最终只会遍历各个字符。
【解决方案2】:

我查看了很多由 Java 和 .Net 开发人员编写的 Python 代码,并且我反复看到了一些我可能会警告/通知您的问题:

Python 不是 Java

不要将所有内容都包装在一个类中:

似乎当 Java 开发人员开始编写 Python 时,即使是最简单的函数也会被封装在一个类中。 Python is not Java. 不要写 getter 和 setter,这就是属性装饰器的用途。

在考虑编写类之前,我有两个谓词:

  1. 我正在将状态与功能结合起来
  2. 我希望有多个实例(否则模块级 dict 和函数就可以了!)

不要对所有内容进行类型检查

Python 使用鸭子类型。参考数据模型。它的内置类型强制是你的朋友。

不要将所有内容都放在 try-except 块中

只捕获你知道你会得到的异常,在任何地方使用异常来控制流在计算上是昂贵的并且可以隐藏错误。 Try to use the most specific exception you expect you might get. 从长远来看,这会导致代码更加健壮。

了解内置类型和方法,尤其是:

From the data-model

str

  • join
  • 只需执行 dir(str) 并全部学习。

list

  • append(在列表末尾添加一项)
  • extend(通过将每个项目添加到可迭代对象中来扩展列表)

dict

  • get(提供一个默认值,让您不必捕获关键错误!)
  • setdefault(从默认值或已经存在的值设置!)
  • fromkeys(使用可迭代键的默认值构建一个字典!)

set

集合包含唯一(无重复)可散列对象(如字符串和数字)。思考维恩图?想知道一组字符串是否在一组其他字符串中,或​​者重叠是什么(或不是?)

  • union
  • intersection
  • difference
  • symmetric_difference
  • issubset
  • isdisjoint

只需对遇到的每种类型执行dir() 即可查看其命名空间中的方法和属性,然后对属性执行 help() 以查看其作用!

了解内置函数和标准库:

I've caught developers writing their own max functions and set objects. It's a little embarrassing. Don't let that happen to you!

标准库中需要注意的重要模块有:

  • os
  • sys
  • collections
  • itertools
  • pprint(我一直在用)
  • logging
  • unittest
  • re(正则表达式在为许多用例解析字符串方面非常高效)

阅读文档以简要了解标准库,这里是 Part 1,这里是 Part II。一般来说,将浏览所有文档作为早期目标。

阅读风格指南:

只需阅读您的风格指南,您就会学到很多关于最佳实践的知识!我推荐:

此外,您可以通过使用“最佳实践”一词在 Google 上搜索您正在研究的问题,然后选择获得最多支持的相关 Stackoverflow 答案,从而学习出色的风格!

祝你在学习 Python 的道路上一帆风​​顺!

【讨论】:

    【解决方案3】:

    例如,我正在编写一个接受 2D Numpy 数组的函数。然后在函数体中,我调用这个数组的不同方法(它是 Numpy 中的数组对象)。但是将来假设我想使用这个函数,到那时我可能完全忘记了我应该将什么作为类型传递给函数。人们通常会做什么?他们只是为此编写文档吗?

    您编写文档并适当地命名函数和变量。

    def func(two_d_array): 
        do stuff
    

    还假设我希望将来传递一个类似于数组的对象,通常在 Java 中,一个会实现一个接口,然后让两个类都实现方法。

    你可以这样做。创建一个基类并从其继承,使多个类型具有相同的接口。然而,很多时候,这太过分了,您只需使用 duck typing 代替。使用鸭子类型,重要的是被评估的对象定义了在代码中使用它所需的正确属性和方法。

    请注意,您可以检查 Python 中的类型,但这通常被认为是不好的做法,因为它会阻止您使用 Python 的动态类型系统启用的鸭子类型和其他编码模式。

    【讨论】:

    • 您的第一个论点因您的示例语法无效这一事实而被削弱,并且在修复后仍然存在(1)丑陋和(2)“系统匈牙利表示法”及其所有缺点。确实可以在名称中传达有用的类型信息,但名称应该仍然有意义。
    • 语法无效?你的意思是do stuff 行?我相信那一点是不言自明的……至于您的第二点,该示例旨在非常通用。我希望这里的意图很明确。
    • 不,我指的是2d_array,它不是标识符。
    • 噢!固定的。好收获!
    • 感谢指出!我忘记了“同意的成年人”的说法;P
    【解决方案4】:

    是的,您应该记录您的方法期望的参数类型,并由调用者传递正确类型的对象。在方法中,您可以编写代码来检查每个参数的类型,或者您可以假设它是正确的类型,如果传入的对象不支持您的代码的方法,则依赖 Python 自动抛出异常需要调用它。

    动态类型的缺点是计算机不能进行前面提到的正确性检查。程序员有更大的负担来确保所有参数都是正确的类型。但优点是您在可以将哪些类型传递给您的方法方面具有更大的灵活性:

    • 您可以为特定参数编写支持多种不同类型对象的方法,而无需重载和重复代码。
    • 有时方法并不真正关心对象的确切类型,只要它支持特定的方法或操作——例如,使用方括号进行索引,它适用于字符串、数组和各种其他事物。在 Java 中,您必须创建一个接口,并编写包装类以使各种预先存在的类型适应该接口。在 Python 中,您无需执行任何操作。

    【讨论】:

      【解决方案5】:

      您可以使用assert 来检查条件是否匹配:

      In [218]: def foo(arg):
           ...:     assert type(arg) is np.ndarray and np.rank(arg)==2, \
           ...:         'the argument must be a 2D numpy array'
           ...:     print 'good arg'
      
      In [219]: foo(np.arange(4).reshape((2,2)))
      good arg
      
      In [220]: foo(np.arange(4))
      ---------------------------------------------------------------------------
      AssertionError                            Traceback (most recent call last)
      <ipython-input-220-c0ee6e33c83d> in <module>()
      ----> 1 foo(np.arange(4))
      
      <ipython-input-218-63565789690d> in foo(arg)
            1 def foo(arg):
            2     assert type(arg) is np.ndarray and np.rank(arg)==2, \
      ----> 3         'the argument must be a 2D numpy array'
            4     print 'good arg'
      
      AssertionError: the argument must be a 2D numpy array
      

      最好像@ChinmayKanchi 提到的那样完整地记录您所写的内容。

      【讨论】:

      • 虽然在技术上是正确的并且偶尔是合适的,但类型检查应该始终作为 Python 中的最后手段。如果就函数而言,对象的行为与 numpy 数组相同,则实际的类型层次结构应该无关紧要。
      • @ChinmayKanchi 如何检查参数的行为是否与 numpy 数组完全相同?
      • 你不会,除非你有特定的理由期望一个函数在程序的正常操作下可能会收到无效的参数。你只是假设调用者知道他/她在做什么,当调用者做了一些意想不到的事情时,让 Python 抛出一个TypeError/KeyError/NameError
      • @zhangxaochen 通常情况下,你不会。在某些情况下,存在抽象基类。请注意,您与静态类型系统的公平比较需要对“完全相同的行为”算作宽容,因为静态类型系统检查一些不变量,但远非全部(例如抛出的异常,解释论据)。
      • 实际上,我认为大多数函数确实可以在一种类型或非常有限的一组类型上运行。
      【解决方案6】:

      这里有一些建议可以帮助您使您的方法更“Pythonic”。

      政治人物

      一般来说,我建议至少浏览一下PEPs。它对我深入了解 Python 帮助很大。

      指针

      既然你提到了指针这个词,Python 并没有像 C 使用指针那样使用指向对象的指针。我不确定与 Java 的关系。 Python 使用names attached to objects。如果您期望 similar-to-C 指针行为,这是一个微妙但重要的区别,可能会给您带来问题。

      鸭子打字

      正如您所说,是的,如果您期望某种类型的输入,请将其放在 docstring 中。

      正如 zhangxaochen 所写,您可以使用 assert 来实时输入您的参数,但如果您一直在没有特别的理由这样做,那并不是真正的 Python 方式。正如其他人提到的,如果必须这样做,最好测试并引发 TypeError。 Python 更喜欢使用鸭子类型 - 如果你给我发一些类似 numpy 2D 数组的东西,那很好。

      【讨论】:

      • 实际上,就与其他语言概念的类比而言,“指向对象的指针”非常好,我不知道由此引起的任何误解或问题。但实际上,在“名称攻击对象”的意义上,Python 对象的行为与 Java 对象几乎相同。
      • 如果你还没有看到,看看this question 和 cmets 的里程,你会发现那里实际上存在很多分歧。来自 C 背景,这对我来说是区分指针和名称的重要一步。与 C 相比,名称当然不是指针。我不知道其他语言如何使用“指针”这个词。
      • 确实 Python 的名称更受限制,没有指针对指针的等价物(更不用说更深的嵌套)。但除此之外,名称的行为类似于指向抽象 struct PyObject 的指针,这并不奇怪,因为它们正是如何实现的。
      • zhangxaochen的回答不正确。不要以这种方式使用断言。
      猜你喜欢
      • 2011-08-14
      • 1970-01-01
      • 2011-06-20
      • 2021-10-15
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多