翻译:如何在Jupyter notebook中安装Python包?

Installing Python Packages from a Jupyter Notebook

写在前面

This notebook originally appeared as a post on the blog Pythonic Perambulations.

本文由我完成翻译,以帮助更多中国朋友解决这个问题。但由于知识和能力的限制,译文很可能存在错误和纰漏,造成对部分朋友的误导。因此如果你有什么意见或建议,欢迎联系我以完善译文。如果我的译文使你产生了困惑,请参考上述原文。

一些额外的说明:

  1. 考虑到它们在计算机领域的常见性,文中除了首次出现的地方,kernel和shell一律使用英文;
  2. 考虑到Jupyter notebook的名称,文中的notebook一律不翻译;
  3. 文中的小标题都没有翻译。

综述

在软件开发领域,有句话叫做一切抽象都是有漏洞的(译者注:这是指在软件开发过程中,本应隐藏实现细节的抽象化不可避免地暴露出底层细节与局限性。抽象泄露是棘手的问题,因为抽象化本来目的就是向用户隐藏不必要公开的细节),如同其他软件一样,这句话对Jupyter notebook也同样适用。

下面这种情况的频繁出现证明了这一点:

我安装了XX包 但是我不能在notebook中导入它,谁能帮帮我?

这个问题多年来在StackOverflow上盛久不衰 (比如 这一个, 这一个, 还有这个, 和这个, 这个, 这个, 以及这一个, 和这个… etc.)。

从本质上来说,这个问题通常源于这么一个事实:Jupyter的内核(kernel)和Jupyter的壳(shell)是不相连的;换句话说,安装程序指向的是另一个Python的版本,而非你在notebook中使用的那一个。
在最简单的情况下这种问题并不会出现,但一旦它出现了,解决这个问题就需要你具有关于操作系统、Python包的安装、以及Jupyter本身的相关知识。
换言之,Jupyter notebook,与其他所有的抽象化的软件一样,也存在漏洞。

在与同事们针对这个问题进行了一些讨论后——有的是线上(这是我们两次线上讨论的链接:讨论 A, 讨论 B), 有的是线下——我决定在这里对这个问题进行一些深入的讲解。

这篇博文旨在阐述清楚以下内容:

  • 首先, 我会针对这个问题提供过一个快速、简单的解决办法,我要怎样通过pip或者conda在我的jupyter notebook上安装一个python包呢?

  • 其次,我会深入这些问题的具体背景:Jupyter notebook的抽象化究竟在做什么, 它是如何与复杂的操作系统进行交互的,以及你要怎样看待这所谓的泄漏,从而对问题出现的前因后果有一个完整的了解。

  • 第三,我会谈一些我的想法,也许开源社区可以考虑一下这些点子以消除这个问题,这包括了Jupyter,Pip,和Conda的开发者需要做的一些改进,来减轻使用者对其产品的认知负担。

这篇文章会专注于安装Python包的两个途径: pipconda.
其他管理包的方式当然也存在 (包括特定平台的工具,比如 yum, apt, homebrew, etc., 以及跨平台的工具 enstaller),但我对它们并不那么熟悉,因此不会再对他们进行深入的讨论。

Quick Fix: How To Install Packages from the Jupyter Notebook

如果你只是想快速解决这个问题,即如何在notebook里安装一个包,那么只看这里就够了。

pip vs. conda

首先,我简单提一下pip 还是 conda的问题。

对很多使用者来说,选择pip还是conda是非常令人困惑的问题。

去年,针对这个问题我写了一篇详尽得可能超过你想象的博文 , 但这二者的本质区别可以这样概括:

  • pip 可以在所有环境下安装python包。
  • conda可以在conda环境下安装所有包。

如果你已经安装了python,那么这个选择对你来说是非常容易的:

  • 如果你是用Anaconda或者Miniconda安装的python,那么请使用conda命令来安装python包。如果conda告诉你你要下载的这个包不存在,那么使用pip
    (或者试试 conda-forge, 它比conda默认的包的数量要更多)。

  • 如果你是使用其他方式安装的python,那么使用pip来安装python包。

最后,因为这样的问题常常发生,我必须提醒你永远不要使用sudo pip install

永远不要。

即便这么做在短期内看起来好像解决了问题,但它总是会在更长的时间范围下带来问题。
比如,如果pip install给了你一个许可错误(permission error),这很可能意味着你在试图安装/升级系统python中的包,比如usr/bin/python。这么做会带来糟糕的后果,因为通常操作系统本身就依赖着某种特定版本的包。
对于日常的python使用,你应该把你的包和系统python的隔离开来,使用 虚拟环境 or Anaconda/Miniconda — 这种场合下我比较喜欢用conda,但我也知道我的很多同事会偏爱virtualenv。

How to use Conda from the Jupyter Notebook

如果你在使用jupyter notebook并且你希望通过conda来安装某个包,你也许会想要使用!来直接在notebook里以shell的方式运行conda:

# DON'T DO THIS!
!conda install --yes numpy
Fetching package metadata ...........
Solving package specifications: .

# All requested packages already installed.
# packages in environment at /Users/jakevdp/anaconda/envs/python3.6:
#
numpy                     1.13.3           py36h2cdce51_0  

(注意:当conda请求用户确认时,我们使用 --yes 来自动回答 y )

出于多种原因(这些原因我将在下面详细介绍),总之,如果你在当前的notebook中使用这样安装的包,这种操作通常来说不会起作用,尽管在很简单的情况下它会起作用。

而以下才是通用的安装方式:

# Install a conda package in the current Jupyter kernel
import sys
!conda install --yes --prefix {sys.prefix} numpy
Fetching package metadata ...........
Solving package specifications: .

# All requested packages already installed.
# packages in environment at /Users/jakevdp/anaconda:
#
numpy                     1.13.3           py36h2cdce51_0  

这些增加的内容确保了conda能在当前运行的Jupyter核内安装这个包。 (感谢 Min Ragan-Kelley 提出了这个办法).

稍后我将讨论为什么这么做是必须的。

How to use Pip from the Jupyter Notebook

如果你在使用Jupyter notebook并且希望通过pip来安装一个包,与上面类似,你也许同样会想用如下方式在shell里直接运行pip:

# DON'T DO THIS
!pip install numpy
Requirement already satisfied: numpy in /Users/jakevdp/anaconda/envs/python3.6/lib/python3.6/site-packages

出于多种原因(这些原因我将在下面详细介绍),总之,如果你在当前的notebook中使用这样安装的包,这种操作通常来说不会起作用,尽管在很简单的情况下它会起作用。

而以下才是通用的安装方式:

# Install a pip package in the current Jupyter kernel
import sys
!{sys.executable} -m pip install numpy
Requirement already satisfied: numpy in /Users/jakevdp/anaconda/lib/python3.6/site-packages

增加的内容确保了你在使用与当前Python核相关的pip版本,因此你所安装的包能在当前notebook中使用。
这实际上源于如下这样一个事实,即时不考虑Jupyter notebook,使用

$ python -m pip install <package>

来安装一个包也要比

$ pip install <package>

更好。

因为前者更清晰地指明了这个包将安装的位置(这一点之后也会详细阐述)。

The Details: Why is Installation from Jupyter so Messy?

上面这些解决办法应该能够应对所有情况……但是为什么我们非得这么做?
简单来说,这是因为在Jupyter里,shell环境和python可执行文件是分离的
想要理解为什么这一点造成了我们现在面对的情况需要你对如下几个不同概念有一个基本的了解:

  1. 你的操作系统是如何定位可执行程序的,
  2. Python是如何安装并定位包的,
  3. Jupyter是如何确定使用哪一个Python可执行文件的。

为求完整性,我将对上述每个问题做一些简单的探究(这里的讨论部分源于
我去年写的 这个StackOverflow上的回答 ).

注意:以下讨论基于Linux,Unix,MacOSX和其他类似的操作系统。Windows有一个略微不同的体系结构,因此在一些细节上会存在差异。

How your operating system locates executables

当你在终端输入诸如 python, jupyter, ipython, pip, conda 这样的命令时,你的操作系统会根据它所具有的一套定义明确的机制来找到这些命令所对应的可执行文件。

在Linux & Mac操作系统中,系统会首先寻找是否有一个别名(alias)与这个命令匹配;如果它没能在$PATH$环境变量中找到:

!echo $PATH
/Users/jakevdp/anaconda/envs/python3.6/bin:/Users/jakevdp/anaconda/envs/python3.6/bin:/Users/jakevdp/anaconda/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin

$PATH中罗列了用来查找可执行文件的目录:举例来说,如果我在我的系统上用上面的$PATH输入python,它首先会寻找/Users/jakevdp/anaconda/envs/python3.6/bin/python,如果这个不存在,那么它会寻找/Users/jakevdp/anaconda/bin/python等等。

(顺带说明:为什么$PATH的第一个条目重复了两次?这是因为每次你打开jupyter notebook的时候,Jupyter会把jupyter可执行文件的地址放到$PATH的开头。在我们的例子中,这个地址已经在路径的开头了,于是结果就是这个条目将会重复。重复的条目可能会造成一些混乱,但也没什么坏处)。

如果你想知道当你输入python的时候,究竟是哪个文件被执行,你可以使用typeshell命令:

!type python
python is /Users/jakevdp/anaconda/envs/python3.6/bin/python

注意,这对你在终端中使用的任何命令都适用:

!type ls
ls is /bin/ls

即使是内置的命令,比如type本身:

!type type
type is a shell builtin

你可以选择增加一个-a标签,来查看指令的所有可执行的版本;比如:

!type -a python
python is /Users/jakevdp/anaconda/envs/python3.6/bin/python
python is /Users/jakevdp/anaconda/envs/python3.6/bin/python
python is /Users/jakevdp/anaconda/bin/python
python is /usr/bin/python
!type -a conda
conda is /Users/jakevdp/anaconda/envs/python3.6/bin/conda
conda is /Users/jakevdp/anaconda/envs/python3.6/bin/conda
conda is /Users/jakevdp/anaconda/bin/conda
!type -a pip
pip is /Users/jakevdp/anaconda/envs/python3.6/bin/pip
pip is /Users/jakevdp/anaconda/envs/python3.6/bin/pip
pip is /Users/jakevdp/anaconda/bin/pip

当你有一个命令的多个不同版本时,了解$PATH在选择“究竟哪一个将被使用”上的重要角色将尤为重要。

How Python locates packages

Python使用一个类似的机制来定位导入的包。
Python搜索的路径列表可在sys.path中找到:

import sys
sys.path
['',
 '/Users/jakevdp/anaconda/lib/python36.zip',
 '/Users/jakevdp/anaconda/lib/python3.6',
 '/Users/jakevdp/anaconda/lib/python3.6/lib-dynload',
 '/Users/jakevdp/anaconda/lib/python3.6/site-packages',
 '/Users/jakevdp/anaconda/lib/python3.6/site-packages/schemapi-0.3.0.dev0+791c7f6-py3.6.egg',
 '/Users/jakevdp/anaconda/lib/python3.6/site-packages/setuptools-27.2.0-py3.6.egg',
 '/Users/jakevdp/anaconda/lib/python3.6/site-packages/IPython/extensions',
 '/Users/jakevdp/.ipython']

默认情况下,Python搜索模块的第一个位置是一个空路径,代表当前工作目录。

如果在这个位置该模块没有被找到,那么它会沿列表向下直到找到为止。

你可以通过导入的模块的__path__属性来获知究竟是哪个地址正在被使用:

import numpy
numpy.__path__
['/Users/jakevdp/anaconda/lib/python3.6/site-packages/numpy']

在大多数情况下,你使用pip或者conda安装的Python包会存放在一个名为site-packages的目录下。重要的一点是,每个Python可执行文件都有它自己的site-packages:这就意味着,当你安装一个包时,它会与某一个特定的Python可执行文件相关联,并且在默认情况下只能在这个Python版本下使用!

我们可以通过对我的每一个可用的python来打印其sys.path来验证这一点,这得益于Jupyter的一个令人振奋的能力,那就是能够在一个代码块中把Python和bash命令结合:

paths = !type -a python
for path in set(paths):
    path = path.split()[-1]
    print(path)
    !{path} -c "import sys; print(sys.path)"
    print()
/Users/jakevdp/anaconda/envs/python3.6/bin/python
['', '/Users/jakevdp/anaconda/envs/python3.6/lib/python36.zip', '/Users/jakevdp/anaconda/envs/python3.6/lib/python3.6', '/Users/jakevdp/anaconda/envs/python3.6/lib/python3.6/lib-dynload', '/Users/jakevdp/anaconda/envs/python3.6/lib/python3.6/site-packages']

/usr/bin/python
['', '/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python27.zip', '/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7', '/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/plat-darwin', '/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/plat-mac', '/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/plat-mac/lib-scriptpackages', '/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/lib-tk', '/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/lib-old', '/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/lib-dynload', '/Library/Python/2.7/site-packages', '/System/Library/Frameworks/Python.framework/Versions/2.7/Extras/lib/python', '/System/Library/Frameworks/Python.framework/Versions/2.7/Extras/lib/python/PyObjC']

/Users/jakevdp/anaconda/bin/python
['', '/Users/jakevdp/anaconda/lib/python36.zip', '/Users/jakevdp/anaconda/lib/python3.6', '/Users/jakevdp/anaconda/lib/python3.6/lib-dynload', '/Users/jakevdp/anaconda/lib/python3.6/site-packages', '/Users/jakevdp/anaconda/lib/python3.6/site-packages/schemapi-0.3.0.dev0+791c7f6-py3.6.egg', '/Users/jakevdp/anaconda/lib/python3.6/site-packages/setuptools-27.2.0-py3.6.egg']

这里的具体细节并不十分重要,但你必须知道每一个Python可执行文件都有其独有的路径,并且除非你修改了sys.path(如果要这么做你必须格外小心)你是不可能在另一个Python环境中导入包的。

当你使用pip install或者conda install的时候,这些命令与具体的Python版本紧密相关:

  • pip为同一路径下的Python安装包
  • conda为当前是激活状态的conda环境安装包

因此,我们可以看到,下面这个pip install会为这个名为python3.6的conda环境执行安装操作:

!type pip
pip is /Users/jakevdp/anaconda/envs/python3.6/bin/pip

与此同时,conda install也能做同样的事情,因为python3.6正是当前激活状态下的环境(注意*代表被激活的环境):

!conda env list
# conda environments:
#
python2.7                /Users/jakevdp/anaconda/envs/python2.7
python3.5                /Users/jakevdp/anaconda/envs/python3.5
python3.6             *  /Users/jakevdp/anaconda/envs/python3.6
rstats                   /Users/jakevdp/anaconda/envs/rstats
root                     /Users/jakevdp/anaconda

这里pipconda都能默认为conda的python3.6环境安装包的原因是这个环境就是我运行notebook的环境。

我要重述一遍这句话以示强调:Jupyter notebook的shell环境与用于运行这个notebook的环境是匹配的。

How Jupyter executes code: Jupyter Kernels

下一个相关的问题就是,Jupyter怎样执行Python代码。这个问题把我们引向了Jupyter kernel的概念。

一个Jupyter kernel是这样一组文件:它们能引导Jupyter找到在notebook中执行代码的方法。
对于Python kernel,他们会指向一个特定的Python版本,但Jupyter的能力比这要更强大:Jupyter具有针对多种语言的许多可用的kernel ,这些语言包括Python2, Python3,Julia, R,Ruby,Haskell,甚至C++和Fortran!

!jupyter kernelspec list
Available kernels:
  python3       /Users/jakevdp/anaconda/envs/python3.6/lib/python3.6/site-packages/ipykernel/resources
  conda-root    /Users/jakevdp/Library/Jupyter/kernels/conda-root
  python2.7     /Users/jakevdp/Library/Jupyter/kernels/python2.7
  python3.5     /Users/jakevdp/Library/Jupyter/kernels/python3.5
  python3.6     /Users/jakevdp/Library/Jupyter/kernels/python3.6

上面罗列的每一个kernel都是一个目录,在这些目录下包含了一个叫做kernel.json的文件,文件中制指定了这个kernel应该使用哪种语言和可执行文件。
比如:

!cat /Users/jakevdp/Library/Jupyter/kernels/conda-root/kernel.json
{
 "argv": [
  "/Users/jakevdp/anaconda/bin/python",
  "-m",
  "ipykernel_launcher",
  "-f",
  "{connection_file}"
 ],
 "display_name": "python (conda-root)",
 "language": "python"
}

如果你想要创建一个新的kernel,你可以使用jupyter ipykernel命令
举个例子,我使用如下命令为模板为我主要的conda环境创建上述kernel:

$ source activate myenv
$ python -m ipykernel install --user --name myenv --display-name "Python (myenv)"

The Root of the Issue

现在我们已经有了足够的背景知识来回答这个问题:*为什么使用!pip install或者!conda install通常都对notebook不奏效?

问题的根源是:当Jupyter notebook开始运行,shell环境就已经被确定了,然而Python的可执行文件却要由kernel来决定,而这二者并不一定互相匹配。
换言之,你并不能保证你$PATH中的pythonpipconda会和notebook中的python相适配。

回忆一下,你路径下的python可以通过如下命令来确定:

!type python
python is /Users/jakevdp/anaconda/envs/python3.6/bin/python

notebook中的Python可执行文件则可以通过如下命令来确定:

sys.executable
'/Users/jakevdp/anaconda/bin/python'

在我当前的notebook环境中,这两个Python并不相同。
这就是为什么简单的!pip install或者!conda install会不奏效:这两个命令会把包安在错误的Python的site-packages下。

如我们上面所说,我们可以通过明确确定安装位置来克服这个问题。
conda中,你可以手动在shell命令中这样增加前缀:

$ conda install --yes --prefix /Users/jakevdp/anaconda numpy

或者,为了能够自动使用正确的前缀(使用notebook中可用的语法):

!conda install --yes --prefix {sys.prefix} numpy

对于pip,你可以明确指出你需要的Python版本:

$ /Users/jakevdp/anaconda/bin/python -m pip install numpy

或者,为了能够自动使用正确版本(又一次使用notebook的shell语法):

!{sys.executable} -m pip install numpy

记住这一点:如果你想安装能在当前notebook中使用的包,你需要你的安装命令当前Python的kernel相匹配。

Some Modest Proposals

因此,总括地说,在Jupyter notebook中安装包之所以困难重重,是因为Jupyter的shell和Python的kernel不匹配,这就意味着比起直接pip install或者conda install,你还得做点别的什么。
之前我们提到的特殊情况下的例外,就是指你刚好在kernel所指的同一个Python环境下运行jupyter notebook;在这种情况下,简单的安装命令就会奏效。

尽管我们解决了这个问题,但这个问题仍然把我们置于一个不太好的环境下,因为对于一个新手而言,他会认为这样的事情应该简单如此:安装一个包,然后使用,仅此而已。而这个情况增大了他们的学习曲线,使得Jupyter显得不那么易用。
那么如果我们是开发社区的成员,我们要怎么做才能消除这个问题呢?

我有一些点子,我想其中的一些会有用的:

Potential Changes to Jupyter

如我所说,问题根源在于Jupyter的shell环境和kernel的不匹配。
那么,我们能否为kernel规范发送信号以强制使二者匹配?
So, could we massage kernel specifications such that they force the two to match?

也许可以:比如,这个github issue 向我们展示了一种修改shell变量,使它成为kernel启动的一部分的方法。

简单来说,在你的kernel目录中,你可以增加一个脚本kernel-startup.sh,它的内容大致如下(注意修改权限以使之可执行):

#!/usr/bin/env bash

# activate anaconda env
source activate myenv

# this is the critical part, and should be at the end of your script:
exec python -m ipykernel $@

之后在你的kernel.json文件中,修改argv的内容如下:

"argv": [
   "/path/to/kernel-startup.sh",
   "-f",
   "{connection_file}"
 ]

一旦你做了这些,转入myenv的kernel会自动激活myenv的conda环境,这会改变你的 $CONDA_PREFIX$PATH 以及其他的系统变量,这会使!conda install XXX!pip install XXX 能够正确工作。在virtualenvs或者其他Python环境中也可以使用类似的方法。

这里有一个麻烦的问题:如果你的myenv环境下没有ipykernel包,也许还得有与之匹配的用于打开notebook的jupyter版本,这个方法将不会有用。因此这不是一个完美的解决办法,不过如果Python kernels能够被设定为可以默认执行这样的shell初始化工作,对用户而言这就不会那么让人困惑了:!pip install!conda install就够了。

Potential Changes to pip

即便在Jupyter之外,关于安装的困惑也并不鲜见。它的一个来源就是,由于系统别名和环境变量的特性,pippython可能会指向不同的路径。
在这样的情况下,pip install会在一个python无法连接的地址安装包。
因此,使用python -m pip install会是一个更加安全的做法。它明确指定了需要安装包的Python版本(毕竟明确总比模糊要好)。

这就是Python文档里不再有pip install,像David Beazley这样经验丰富的Python教学者从来不教的“裸露的pip”(bare pip)的一个原因。
Cpython开发者Nick Colghlan甚至指出总有一天pip会被python -m pip取代。
尽管这样做命令好像更冗长了,但我还是认为强制用户使用更明确的命令会是一个很有用的改变,尤其是在当下virtualenvs和conda正在日渐普遍的情况下。

Changes to Conda

我认为对conda的API做一些修改会对用户来说很有帮助。

Explicit invocation

pip相对应,我认为使用python -m conda install会与上面pip的情况一样。
你可以在root环境下这样使用conda命令,但是conda的python包(与conda可执行文件相对)目前不能在root环境以外的地方安装:

(myenv) jakevdp$ conda install conda
Fetching package metadata ...........

InstallError: Error: 'conda' can only be installed into the root environment

我猜测在所有conda环境下允许python -m conda install会需要对conda的安装模型做很大的重构工作,因此也许仅仅为了与pip的API对应而做的改动并不划算。
即便如此,这样的对应对用户而言也会是很大的帮助。

A pip channel for conda?

另一个conda可以做的改动可能是增加一个Python包目录的镜像通道,这样一来,当你使用conda isntall some-package的时候,计算机会自动像pip一样提取软件包。

我对conda架构的了解还不够深入,因此我不知道增加这样的特性的难度会有多大。但我毫无疑问具有帮助新手接触Python和/或conda的丰富经验:我有把握说增加这样的特性能够使他们的学习曲线更加平稳。

New Jupyter Magic Functions

即使上述这些改动都不可能实现的,我们还是可以通过在Jupyter notebook中引入%pip%conda魔法命令来简化用户体验。这样的魔法命令会检测当前的kernel并使得软件包能被安装在正确的位置。

pip magic

举个例子,你可以像下面这样定义一个%pip魔法命令,它将能够在当前kernel中使用:

from IPython.core.magic import register_line_magic

@register_line_magic
def pip(args):
    """Use pip from the current kernel"""
    from pip import main
    main(args.split())

按如下方式运行会在预期的位置安装软件包:

%pip install numpy
Requirement already satisfied: numpy in /Users/jakevdp/anaconda/lib/python3.6/site-packages

值得一提的是,Jupyter开发者Matthias Bussonnier已经在他的pip_magic代码仓库中发表了这样的内容,因此你可以通过

$ python -m pip install pip_magic

安装,并且现在就使用它(当然,前提是你能够把pip_magic安装在正确的位置!)

conda magic

类似地,我们可以定义一个这样一个conda魔法命令:如果你输入%conda install XXX,它也能帮你在正确的位置完成安装。
这会比pip魔法命令更复杂一点,因为它必须保证当前的环境是兼容conda的,并且(与之前所说的python -m conda install无法使用的问题相关)必须唤起一个子进程来执行正确的shell命令:

from IPython.core.magic import register_line_magic
import sys
import os
from subprocess import Popen, PIPE


def is_conda_environment():
    """Return True if the current Python executable is in a conda env"""
    # TODO: make this work with Conda.exe in Windows
    conda_exec = os.path.join(os.path.dirname(sys.executable), 'conda')
    conda_history = os.path.join(sys.prefix, 'conda-meta', 'history')
    return os.path.exists(conda_exec) and os.path.exists(conda_history)


@register_line_magic
def conda(args):
    """Use conda from the current kernel"""
    # TODO: make this work with Conda.exe in Windows
    # TODO: fix string encoding to work with Python 2
    if not is_conda_environment():
        raise ValueError("The python kernel does not appear to be a conda environment.  "
                         "Please use ``%pip install`` instead.")

    conda_executable = os.path.join(os.path.dirname(sys.executable), 'conda')
    args = [conda_executable] + args.split()

    # Add --prefix to point conda installation to the current environment
    if args[1] in ['install', 'update', 'upgrade', 'remove', 'uninstall', 'list']:
        if '-p' not in args and '--prefix' not in args:
            args.insert(2, '--prefix')
            args.insert(3, sys.prefix)

    # Because the notebook does not allow us to respond "yes" during the
    # installation, we need to insert --yes in the argument list for some commands
    if args[1] in ['install', 'update', 'upgrade', 'remove', 'uninstall', 'create']:
        if '-y' not in args and '--yes' not in args:
            args.insert(2, '--yes')

    # Call conda from command line with subprocess & send results to stdout & stderr
    with Popen(args, stdout=PIPE, stderr=PIPE) as process:
        # Read stdout character by character, as it includes real-time progress updates
        for c in iter(lambda: process.stdout.read(1), b''):
            sys.stdout.write(c.decode(sys.stdout.encoding))
        # Read stderr line by line, because real-time does not matter
        for line in iter(process.stderr.readline, b''):
            sys.stderr.write(line.decode(sys.stderr.encoding))

You can now use %conda install and it will install packages to the correct environment:

现在你就可以使用%conda install在正确环境下安装软件包了:

%conda install numpy
Fetching package metadata ...........
Solving package specifications: .

# All requested packages already installed.
# packages in environment at /Users/jakevdp/anaconda:
#
numpy                     1.13.3           py36h2cdce51_0  

这个conda魔法命令还需要一些工作才能用作一个通用的解决办法(参考代码中的TODO注释),但我认为这是一个很有意义的开始。

如果这样的pip和conda魔法命令能够加入Jupyter默认的魔法命令集,我认为它将在解决在Jupyter notebook中安装Python包的道路上迈出一大步。
然而这样的方法也不是完全安全的:这几个魔法命令只不过是另一层抽象而已,像一切抽象一样,它们也不可避免地存在泄漏的问题。
但如果能很谨慎地实现它们,我认为将大大改善用户体验。

Summary

在这篇博文中,我试图一次性解答这个多年来盛久不衰的问题,我要怎么在Jupyter notebook里安装Python包?

在提出了一些可以实现的简单解决办法后,我深入探究了为什么我们需要这么做:这归根结底来源于Jupyter的kernel是与shell不相连的。

kernel环境在执行期间可能会改变,但shell环境在Jupyter开始运行时就已经被确定了。
一个完整的解释需要花费如此巨大的篇幅,并且需要引入这么多概念,在我看来已经揭示了Jupyter生态的一个真实存在的缺陷,因此我为社区开发者们提出了一些可行的办法,来优化用户体验。

最后必须补充一点:我对Jupyter,conda,pip和其他一些构成了Python数据科学生态的工具的开发者们具有崇高的尊重和敬意。
我非常确定,这些开发者们此前就考虑过这些问题,并且权衡过这些可能的修复办法——如果你读到了这篇文章,请不要有所顾忌,我非常欢迎来自您的评论,也许您能指出一些我忽略了的东西。
最后,感谢你们为开源社区所做的贡献。

Thanks to Andy Mueller, Craig Citro, and Matthias Bussonnier for helpful comments on an early draft of this post.

This post was written within a Jupyter notebook; you can view a static version here or download the full notebook here.

感谢原作者提供翻译许可。

This blog is under a CC BY-NC-SA 3.0 Unported License
Link to this article: http://huangweiran.club/2018/05/14/翻译:如何在Jupyter-notebook中安装Python包?/