【Python】python 避坑指南 | 不断补充中

Python 避坑指南

Overview

说在前面的话

Python 是一门高度“一致性”的语言。这主要归功于编写类时可以重写内置方法。
所以使用 Python 起来几乎可以“凭感觉”,写得好的库,基本上就是感觉它应该是怎么用的,就那样先使用再说,出问题了再去看用法是否不同也无妨。

但也正是因为如此,如果这样就误以为不用深入理解 Python 那么就很容易误写一些代码。

本文不定期收集/更新那些“反直觉”的 Python 编程陷阱。

Note:
本篇文章假设读者已经有了一些编程经验和知识,因此有部分编程(计算机)领域的专业名词不会专门再做解释。
比如:类型,变量,参数,指针,内存 等这些概念本篇都假设读者已经理解。
以及为了更好地理解本篇,建议读者知道 Python 中的可变数据类型和不可变数据类型的概念。


0 Python 中 OO

OO => Object-Oriented 即面向对象。

原本这篇博客我没打算写这一小节,但是仔细考虑之后,如果没有建立 OO 的概念和理解其 OO 中常用的名词,那么这篇文章可能有部分小节读起来不知所云。
所以开头做了一些基本知识的补充,已熟知的读者可以略过这一节。

我们知道 Python 很灵活,你既可以将它用作面向过程式编程,也可以用作面向对象式地编程,当然也可以函数式编程。

如果我们将 Python 仅以面向过程式地编程,那么本文的大部分问题可能有不会遇到。
但是实际上,当我们的软件稍微复杂一点点,就会很自然地使用起了面向对象编程。

我不打算深谈 OO,因为这样就脱离了本文的重点。

以下是我认为在阅读本文前需要理解的 OO 的概念:

  • 类不是对象(类是用来创建对象的模型);
  • Python 中所有变量都是对象;
  • 类中的变量也称为“实例变量”,或“状态”;
  • 类中的函数也称为“方法”,“行为”;

1 开胃小菜 - 学习用 Python 编程时要避免的 3 个错误

本节转载自 LINUX 中国 微信订阅号的一篇文章 ???? 学习用 Python 编程时要避免的 3 个错误

【Python】python 避坑指南 | 不断补充中
为了让初学 Python 的程序员避免犯同样的错误,以下列出了我学习 Python 时犯的三种错误。这些错误要么是我长期以来经常犯的,要么是造成了需要几个小时解决的麻烦。

-- Pete Savage

这些错误会造成很麻烦的问题,需要数小时才能解决。

当你做错事时,承认错误并不是一件容易的事,但是犯错是任何学习过程中的一部分,无论是学习走路,还是学习一种新的编程语言都是这样,比如学习 Python。

为了让初学 Python 的程序员避免犯同样的错误,以下列出了我学习 Python 时犯的三种错误。这些错误要么是我长期以来经常犯的,要么是造成了需要几个小时解决的麻烦。

年轻的程序员们可要注意了,这些错误是会浪费一下午的!

1.1 可变数据类型作为函数定义中的默认参数

笔者注:这个错误是因为 Python 程序在加载的时候(主程序文件和 model 文件)固定了一些全局变量。
更加具体的分析本人还未整理,如果你在阅读之后想要深入理解,这里是一些直接相关的参考:

defclass 都是语句!
深入理解 inspect - Inspect live objects
《流畅的 Python》 - 21.3 导入时和运行时比较。

不会等太久我就会更新这方面主题的文章,如果有读者有了解这方面的好文欢迎在评论区指出。

这似乎是对的?你写了一个小函数,比如,搜索当前页面上的链接,并可选将其附加到另一个提供的列表中。

def search_for_links(page, add_to=[]):
    new_links = page.search_for_links()
    add_to.extend(new_links)
    return add_to

从表面看,这像是十分正常的 Python 代码,事实上它也是,而且是可以运行的。但是,这里有个问题。如果我们给 add_to 参数提供了一个列表,它将按照我们预期的那样工作。但是,如果我们让它使用默认值,就会出现一些神奇的事情。

试试下面的代码:

>>> def fn(var1, var2=[]):
...     var2.append(var1)
...     print var2
>>> fn(3)
>>> fn(4)
>>> fn(5)

笔者注:显然,原文作者是使用 Python 2,但是我相信你有能力改成 Python 3

可能你认为我们将看到:

[3]
[4]
[5]

但实际上,我们看到的却是:

[3]
[3, 4]
[3, 4, 5]

为什么呢?如你所见,每次都使用的是同一个列表,输出为什么会是这样?在 Python 中,当我们编写这样的函数时,这个列表被实例化为函数定义的一部分。当函数运行时,它并不是每次都被实例化。这意味着,这个函数会一直使用完全一样的列表对象,除非我们提供一个新的对象:

>>> fn(3, [4])
[4, 3]

答案正如我们所想的那样。要想得到这种结果,正确的方法是:

def fn(var1, var2=None):
    if not var2:
        var2 = []
    var2.append(var1)

或是在第一个例子中:

def search_for_links(page, add_to=None):
    if not add_to:
        add_to = []
    new_links = page.search_for_links()
    add_to.extend(new_links)
    return add_to

这将在模块加载的时候移走实例化的内容,以便每次运行函数时都会发生列表实例化。请注意,对于不可变数据类型,比如元组、字符串、整型,是不需要考虑这种情况的。这意味着,像下面这样的代码是非常可行的:

def func(message="my message"):
    print message

1.2 可变数据类型作为类变量

笔者注:这个问题的原因是没有了解到定义类时变量是放在 __init__ 内和 __init__ 外的不同。

这和上面提到的最后一个错误很相像。思考以下代码:

class URLCatcher(object):
    urls = []
    
    def add_url(self, url):
        self.urls.append(url)

这段代码看起来非常正常。我们有一个储存 URL 的对象。当我们调用 add_url 方法时,它会添加一个给定的 URL 到存储中。看起来非常正确吧?让我们看看实际是怎样的:

a = URLCatcher()
a.add_url('http://www.google.com')
b = URLCatcher()
b.add_url('http://www.bbc.co.hk')

b.urls

['http://www.google.com', 'http://www.bbc.co.uk']

a.urls:

['http://www.google.com', 'http://www.bbc.co.uk']

等等,怎么回事?!我们想的不是这样啊。我们实例化了两个单独的对象 a 和 b。把一个 URL 给了 a,另一个给了 b。这两个对象怎么会都有这两个 URL 呢?

这和第一个错例是同样的问题。创建类定义时,URL 列表将被实例化。该类所有的实例使用相同的列表。在有些时候这种情况是有用的,但大多数时候你并不想这样做。你希望每个对象有一个单独的储存。为此,我们修改代码为:

class URLCatcher(object):
    def __init__(self):
        self.urls = []

    def add_url(self, url):
        self.urls.append(url)

现在,当创建对象时,URL 列表被实例化。当我们实例化两个单独的对象时,它们将分别使用两个单独的列表。

笔者注:如果你想知道更多:__init__class 的初始化方法,__new__class 的构造方法。
所以当你想要初始化一些类中的参数时,比如: TheClass(param1=value1, param2=value2) 那么你想要的是 “初始化”!
看,通过简单的名字你就能区分它们的功用。
至于什么时候"类的实例变量"放在 __init__ 之外,等你需要的时候你会很自然地知道的。

1.3 可变的分配错误

笔者注:这个问题很简单,如果你是真的学过 Python 你就应该很容易理解下面指出的问题。
如果你觉得以下的问题前所未见,对你很新鲜,那么必须建议你认真严谨地学一遍 Python
(《Learning Python》就是一本 Python 入门的最佳选择)

这个问题困扰了我一段时间。让我们做出一些改变,并使用另一种可变数据类型 - 字典

a = {'1': "one", '2': 'two'}

现在,假设我们想把这个字典用在别的地方,且保持它的初始数据完整。

b = a

b['3'] = 'three'

简单吧?

现在,让我们看看原来那个我们不想改变的字典 a :

{'1': "one", '2': 'two', '3': 'three'}

哇等一下,我们再看看 b

{'1': "one", '2': 'two', '3': 'three'}

等等,什么?有点乱……让我们回想一下,看看其它不可变类型在这种情况下会发生什么,例如一个"元组":

c = (2, 3)
d = c
d = (4, 5)

现在 c(2, 3),而 d(4, 5)

这个函数结果如我们所料。那么,在之前的例子中到底发生了什么?当使用可变类型时,其行为有点像 C 语言的一个指针。在上面的代码中,我们令 b = a,我们真正表达的意思是:b成为 a 的一个引用。它们都指向 Python 内存中的同一个对象。听起来有些熟悉?那是因为这个问题与先前的相似。其实,这篇文章应该被称为「可变引发的麻烦」。

列表也会发生同样的事吗?是的。那么我们如何解决呢?这必须非常小心。如果我们真的需要复制一个列表进行处理,我们可以这样做:

b = a[:]

这将遍历并复制列表中的每个对象的引用,并且把它放在一个新的列表中。但是要注意:如果列表中的每个对象都是可变的,我们将再次获得它们的引用,而不是完整的副本。

假设在一张纸上列清单。在原来的例子中相当于,A 某和 B 某正在看着同一张纸。如果有个人修改了这个清单,两个人都将看到相同的变化。当我们复制引用时,每个人现在有了他们自己的清单。但是,我们假设这个清单包括寻找食物的地方。如果“冰箱”是列表中的第一个,即使它被复制,两个列表中的条目也都指向同一个冰箱。所以,如果冰箱被 A 修改,吃掉了里面的大蛋糕,B 也将看到这个蛋糕的消失。这里没有简单的方法解决它。只要你记住它,并编写代码的时候,使用不会造成这个问题的方式。

字典以相同的方式工作,并且你可以通过以下方式创建一个昂贵副本:

b  = a.copy()

再次说明,这只会创建一个新的字典,指向原来存在的相同的条目。因此,如果我们有两个相同的列表,并且我们修改字典 a 的一个键指向的可变对象,那么在字典 b 中也将看到这些变化。

可变数据类型的麻烦也是它们强大的地方。以上都不是实际中的问题;它们是一些要注意防止出现的问题。在第三个项目中使用昂贵复制操作作为解决方案在 99% 的时候是没有必要的。你的程序或许应该被改改,所以在第一个例子中,这些副本甚至是不需要的。

笔者注:如果你没有理解 “在 Python 中一切对象皆指针”,那么下面这个图你需要牢记 ????
【Python】python 避坑指南 | 不断补充中


作者简介:
Pete Savage - Peter 是一位充满激情的开源爱好者,在过去十年里一直在推广和使用开源产品。他从 Ubuntu 社区开始,在许多不同的领域自愿参与音频制作领域的研究工作。在职业经历方面,他起初作为公司的系统管理员,大部分时间在管理和建立数据中心,之后在 Red Hat 担任 CloudForms 产品的主要测试工程师。


via: https://opensource.com/article/17/6/3-things-i-did-wrong-learning-python
作者:Pete Savage[5] 译者:polebug 校对:wxy
本文由 LCTT 原创编译,Linux中国 荣誉推出


2 深入理解 Python 的不可变对象类型

这是一节在 “不可以变数据类型” 身上误用 “在 Python 中一切对象皆指针” 的特性。
这一节加上上一节的 1.3 可变的分配错误 可以构成“Python 变量在内存中的表现形式”的全集。

2.1 bool 类型(True and False)

虽然我是在一个 50 行左右的实现一个基本功能的代码中发现了自己对于这个“在 Python 中一切对象皆指针” 的误用,但是如果一开始就放个 50 行代码,未免有些太不人性化了。
所以下面我将从一个非常基本的几行代码开始,因为代码行数关系,所以乍看过去可能“不实用”,“不会这么写”,“不会这么用”。但是相信我,如果你不注意这个问题,就迟早要趟这个坑。

如果我们不是直接地像 var1 = True 这样绑定一个 bool 类型;而是在类中有一个变量需要是 bool 类型:

class A:
    quit = False

如果你有仔细地阅读上面的小节,或许你会困惑写成这样会不会更好:

class A:
    def __init__(self):
        self.quit = False

但是没有关系,这两种方式在这个主题下的作用是一样的(在这个实例中我不会去实例化多个 A),只要你知道个中区别就可以了。

然后我们有了这个实例化的类:

instance = A()

当然,这个时候我们可以查看一下:

>>> instance.quit
False

当然,到目前为止没有任何特别的地方。
接下来想象一下如果我们想要绑定一个变量在 instance.quit 上:

is_quit = instance.quit

我们可以预期:

>>> is_quit
False

现在,我们在程序中想要对 “quit” 起到它的字面上的作用,所以:

instance.quit = True

还记得我们上面使用 is_quit 绑定了 instance.quit 吗?根据上面的“变量不是盒子”(“Python 中的一切对象皆指针”),我们可能会预期 is_quit 变量指向的值也改变了:

>>> is_quit

你觉得结果会是 True 吗?
很不幸,答案是否定的,is_quit 仍然是 False

这是为什么呢?
其实上一节已经告诉过我们答案了,只不过它没有具体展开说明 ????
【Python】python 避坑指南 | 不断补充中
当“变量不是盒子”这个在 Python 中的概念已经深入脑髓之后,需要警惕的一点是:这句话本身是对的,但是它作用在“不可变对象”身上的时候,表现出来的现象和作用在“可变对象”完全不同。
考虑下面示例(在手机上查看左滑以显示右侧内容):

>>> a = [1, 2, 3]             ||    >>> a = True
>>> b = a                     ||    >>> b = a
>>> print(b) # ---- I.1       ||    >>> print(b) # ---- II.1
>>> a.append(4)               ||    >>> a = False
>>> print(a) # ---- I.2       ||    >>> print(a) # ---- II.2
>>> print(b) # ---- I.3       ||    >>> print(b) # ---- II.3

那么其中 I.1, I.2, I.3 和 II.1, II.2, II.3 分别输出什么值呢?

希望你的答案是正确的:

I.1 ⇒ [1, 2, 3]               ||  II.1 ⇒ True
I.2 ⇒ [1, 2, 3, 4]            ||  II.2 ⇒ False
I.3 ⇒ [1, 2, 3, 4]            ||  II.3 ⇒ True

造成上述两种截然不同的现象的原因就是一直在说的 Python 中的“可变数据类型” 和 “不可变数据类型”。
这两种类型的本质区别就是在程序运行时在内存地址上的操作形式不同。
以可变数据类型为例:
现在我们想象下图中的右侧是程序运行时拥有的内存 ????

             ┈
          ├---------------------┤
          |     [1, 2, 3]       |
a ------->├--- 2240175921096 ---┤
          |  ┈                  |

可以使用 id 内置函数来获得变量的内存地址:

>>> id(a)
2240175921096

当在python 中执行 b=a 的时候,根据我们上文“变量不是盒子”一图,我们可以很容易地想象出来,这个时候其实是 b 也指向了上面竖型的内存地址。
那么,a.append(4) 发生了什么?

             ┈
          ├---------------------┤
          |     [1, 2, 3, 4]    |
a ------->├--- 2240175921096 ---┤
          |  ┈                  |

没有太多特别的地方,就是在原来的位置上直接增加 4。

当然,这个结构并不是百分百还原,因为列表内部的每个位置也不是直接存值的,而且1, 2, 3, 4 也不是在同一块地址上。
但是,你懂的,如果要画到百分百还原,那么这个图看起来就会比较复杂,不够直观。

如果是不可变数据类型是怎么样:

             ┈
          ├---------------------┤
          |         False       |
a ------->├----- 1630675120 ----┤
          |  ┈                  |

这里没有太多不同,但是如果 a=True 会发生什么:

             ┈
          ├---------------------┤
          |         True        |
a ------->├----- 1630675120 ----┤
          |  ┈                  |

是上图这样吗?当然,实际情况不是的,不可变数据类型不能在原位置上修改。
所以它实际上发生了这样:

             ┈
          ├---------------------┤
          |      True           |
    ╭---->├---  1630675152 -----┤
   /        ┈
  /       ├---------------------┤
 /        |         False       |
a - xx -->├----- 1630675120 ----┤
          |  ┈                  |

所以在不可变数据类型的复制过程中,当 a=False 时,a 指向的内存地址是 1630675120,当 b=a 的时候, b 指向的内存地址也是 1630675120;但是,当 a=True 的时候,正如上图所示,a 指向的内存地址已经发生了改变,可以 b 仍然还是指向原先 False 所在的内存地址,所以 a 是 True, b 是 False。

注意,这里内存的表现形式是十进制,可以使用 hex(id(a)) 来输出 a 变量的二进制形式的内存地址。

关于 bool 类型的更多细节:
有兴趣请看 Guido van Rossum 的博客 The story of None, True and False (and an explanation of literals, keywords and builtins thrown in)

最后,我这一节最开始提到的我个人是如何踩下这一个坑的代码基本上像这样:

# main.py
import signal
import time

class ProgSignal:
    quit = False

progsignal = ProgSignal()

class Work:
    def __init__(self, is_quit):
        self.__is_quit = is_quit

    def want_terminal(self):
        return self.__is_quit

    def run(self, tim=5):
        while True:
            if self.want_terminal():
                break

            self.loop()
            time.sleep(tim)

    def loop(self):
        try:
            print("Hi! Time now: {}".format(time.ctime()))
        except Exception as err:
            print("loop error: {}".format(err)); raise

def main():
    prog_init()

    work = Work(progsignal.quit)
    work.run(1)

def sigint_handler(signo, frame):
    progsignal.quit = True

def prog_init():
    signal.signal(signal.SIGINT, sigint_handler)
    signal.signal(signal.SIGTERM, sigint_handler)

if __name__ == '__main__':
    main()

这个程序的功能就是“时钟功能”,每秒向终端打印当前时间。
你可以看到它是一个死循环,但是因为有捕获了退出信号,并且循环中有判断退出机制。
所以预期是按下 Ctrl+C 之后,退出程序。

当然,上面的程序是错误的使用示范,你能找出哪里用法错误然后修正它吗?