将 Python 作为第二语言:共情(2018)
ballingt
Python 不是 Java 或 C++:将 Python 作为第二语言的共情
2018年4月6日 摘要
Python 不是 Java,也不是 C++
它不一样! 让我们来谈谈如何不同。
为什么?
因为作为 Python 专家(你选择了参加 Python 会议,所以很可能你已经是专家,或者随着时间推移,如果你继续参加 Python 会议,你将会成为专家),我们有责任帮助那些不如我们了解 Python 的同事和合作者。 今天我想关注的责任部分是当其他人有其他编程语言的经验,但是是 Python 新手时。 我在 Dropbox 工作,正如 Guido 今天早些时候所说,这是一家大量使用 Python 的公司。 但是很多程序员来到 Dropbox 时并没有重要的 Python 经验。 当这些人加入时,会花几个月的时间真正专注于学习并弄清楚 Python 是如何工作的,并在这样做的同时获得很多乐趣吗? 那太好了(简要展示 Recurse Center 徽标的幻灯片),但通常情况并非如此。 相反,他们在工作中学习,他们立即开始取得进展。 他们会读一些书(我最喜欢的是 Python Essential Reference,但我听说 Fluent Python 非常棒),观看一些 Python talks,阅读一些 blog posts,在工作中提问,并大量使用 Google。 最后一个是主要的,大量的 Google 和大量的 Stack Overflow。 主要通过 Google 学习可能会让你留下一些盲点。 如果你学习一种语言的方式是通过查找让你困惑的事情,那么那些不明显令人困惑的事情就不会出现。 我们应该努力理解我们的同事对 Python 的理解。 每当你教学时,每当你试图与另一个人交流时,这都是一件大事:试图弄清楚他们对情况的心理模型,并提供正确的概念垫脚石,以将该模型更新到更有用的状态。
Python-as-a-secondary-language 共情
我们应该努力理解那些将 Python 作为一种新语言的人对 Python 的理解。 我将称之为“Python-as-a-second-language empathy.” 我们如何建立这种 PaaSL 共情的东西? 你能做的最好的事情是先学习另一种语言,然后再学习 Python。 这里有谁在学习 Python 之前就精通另一种语言? (大多数人举手)太棒了! 太棒了! 这是你拥有而我永远无法拥有的超能力。 我永远无法放弃学习 Python,精通另一种语言,然后再学习 Python。 你拥有我无法拥有的这种视角。 我鼓励你使用这种超能力来帮助其他具有与你相似背景的人。 我很乐意在 Python 会议上看到“Django for Salesforce Programmers”的演讲,因为在教学时能够与共享的现有知识库建立联系非常有效。 你可以做的另一件事来建立这种 PAASL 共情(我仍在决定一个首字母缩略词)是学习与你所知道的语言不同的语言。 每次你学习一种新语言时,你都在学习某人可能产生误解的新维度。 考虑以下情况:
a = b + c
根据你所知道的语言,你可能会对以下问题的答案做出不同的假设:
- 从现在开始,a 是否始终等同于 b 和 c 的总和,还是仅在我们运行此代码后才为真?
- b + c 会立即评估,还是在稍后使用 a 时评估?
- b 和 c 可能是具有副作用的函数调用吗?
- 哪个会先评估?
- 加号是什么意思,我们如何找出答案?
a
是一个新变量吗? 如果是,它现在是全局变量吗?- 存储在
a
中的值是否知道该变量的名称?
这些是你可以提出的问题,也是某人可能会感到困惑的方式,但是如果你不熟悉以不同方式回答这些问题的语言,你可能无法理解这些误解。 你可以做的另一件事来建立 PSL 共情是倾听。 倾听问题并注意其中的模式。 如果你与了解 R 并且正在学习 Python 的研究生一起工作,请尝试注意哪些问题反复出现。 从广义上讲,这就是我最喜欢的 PyCon 演讲者 Ned Batchelder 做得非常出色的事情。 Ned 是一位圣人,他在 #python irc 频道花费数千小时反复回答有关 Python 的相同问题。 他还做了很多其他事情,比如运营 Boston Python Users Meetup 组织,并将所有这些互动整合到演讲中,这些演讲简洁地涵盖了当年 PyCon 演讲中所有令人困惑的事情。 我将提出的建立 Py2ndLang 共情的最后一个想法是学习你的合作者更了解的语言,以便你更好地想象他们的体验可能是什么样的。 如果你的同事来自 Java,那就去学习 Java! 为了这次演讲,我学习 C++ 和 Java 的工作做得一般。 我做了一些研究,以便我可以尝试向你展示如果从这些语言之一转到 Python,可能会遇到的一些棘手的事情。 我选择这些语言是因为它们是我同事的常用语言。 假设一种编程语言的工作方式与你已经知道的语言类似是非常合理的,因为它们经常这样做! 但是当存在差异时,它会令人惊讶。 C++ 和 Java 不是我的背景! 虽然 Python 是我真正深入研究的第一种语言,但我之前接触过编程,这影响了我学习 Python 的经验。 我的第一种编程语言是 TI-81 Basic,然后是我妈妈教我的一些 Excel。 在 Starcraft 场景编辑器中,你可以使用触发器语言编写程序,所以我做了一些。 在中学时,我可以使用 Microworlds Logo,这非常令人兴奋。 我做了一些 Visual Basic,上了大学并做了一些 MATLAB 和一些 Mathematica,然后我上了一门 CS 课程,他们在那里教我们 Python。 我对 Python 的误解与其他学生截然不同,其中一些人高中时学过带有 Java 的 AP 计算机科学。 我学习的语言都是具有函数作用域的动态类型语言,因此我没有像来自 Java 的人那样出现“我的类型在哪里?”的反应。 Java 和 C++ 是值得关注的好语言,因为它们通常在学校教授,因此在面试或与刚从本科毕业的人一起工作时,尝试理解这些语言可能会很有用。 在我们进入棘手部分列表之前,有一些我不会谈论的事情,因为我不称它们为“棘手”。 不是说它们不难,而是它们不是有害的,它们是被迅速击垮的误解,而不是危险地徘徊。 像冒号和空格这样的新语法,像 yield 这样的新关键字; Python 以 SyntaxErrors 的形式提供有关第一组的反馈,并且可以使用第二个进行 Google 搜索。 当你第一次在 Python 中看到列表推导时,你就知道这种语法有些不正常,因此你知道要研究它或提出有关它的问题。
来自 Java/C++ 的棘手 Python 速查表
让我们将来自 Java 或 C++ 的人认为 Python 棘手的事情分为三类:看起来与 Java 或 C++ 相似但行为不同的事情,行为略有不同的事情,以及不留痕迹的“隐形”事情。 第一类很棘手,因为你可能不会想到要查找任何差异,第二类很棘手,因为你可能会测试差异,并在表面上观察不到任何差异,但实际上潜伏着一些更深层次的差异。 第三类很棘手,因为你正在编辑的文件中没有任何代码可能会引导你进行调查。 这些都是非常随意的类别。
看起来相似,行为不同
装饰器
Java 中有一种称为注释的东西,你可以将其粘贴到方法或类或某些其他东西上。 这是一种向事物添加一些元数据的方法。 然后也许你可以做一些元编程之类的事情,你可以在以后查看该元数据,并根据它们决定运行哪些代码。 但是注释远不如 Python 装饰器强大。
>>> @some_decorator
... def foo():
... pass
...
>>> foo
<quiz.FunctionQuestion object at 0x10ab14e48>
在这里(在 Python 中),一个 Python 装饰器位于一个函数之上,但是输出的是一个自定义类“FunctionQuestion”的实例 - 重要的是要记住装饰器是任意代码,它们可以做任何事情。 来自 Java 的人可能会忽略这一点,认为这是一个添加元数据的注释,而不是在定义时转换函数。
类体赋值创建类变量
我以前见过一些有趣的酷错误,因为这个。 下面这两个赋值是非常不同的:
class AddressForm:
questions = ['name', 'address']
def __init__(self):
self.language = 'en'
questions
是一个类属性,而 language
是一个实例属性。 这些是在 Java 和 C++ 中存在的概念,名称略有不同(questions
可能被称为“静态”变量,而 language
被称为“成员”变量),但是如果你在其中一种语言中看到类似顶部的代码,人们可能会认为你在初始化实例上的属性; 他们可能认为第一件事是做第二件事的另一种方式。
运行时错误,而不是编译时错误
在这里,我稍微拼错了单词“print:”
if a == 2:
priiiiiiiiiiiiint("not equal")
这是有效的 Python 代码,并且直到 a 恰好在运行此代码时为 2,我才会注意到它有任何异常。 我认为来自像 Java 和 C++ 这样具有更多静态检查的语言的人很快就会被它咬伤并害怕它,但是有很多情况需要他们考虑。
try:
foo()
except ValyooooooooooError:
print('whoops)'
在这里,我稍微拼错了 ValueError
,但在 foo()
引发异常之前我不会发现。
try:
foo()
except ValueError:
priiiiiiiiiiiiiiint('whoops)'
这里 ValueError
很好,但是下面的代码直到 foo()
引发异常才会运行。
条件和运行时导入
上述问题的特别可怕的例子是 import
,因为人们可能认为导入的工作方式与它们在 Java 或 C++ 中的工作方式类似:在程序运行之前发生的事情。
try:
foo()
except ValueError:
import bar
bar.whoops()
直到 foo()
引发 ValueError
,我们才会知道 bar 模块的语法是否有效,因为我们尚未加载它,或者是否根本存在名为 bar.py 的文件!
块作用域
如果你主要熟悉 Python,这可能会让你大吃一惊:有一种称为块作用域的概念。 想象一下,每次你缩进时,你都会得到一组新的局部变量,并且每次你取消缩进时,这些变量都会消失。 使用 Java 或 C++ 的人非常习惯这个想法,他们真的期望当他们超出范围时(他们使用花括号来表示,而不是缩进),这些变量会消失。 作为 Python 用户,我们可能知道在下面,
def foo():
bunch = [1, 2, 3, 4]
for apple in bunch:
food = pick(apple)
print(apple)
print(food)
变量 apple
和 bunch
“逃脱”了 for 循环,因为 Python 具有函数作用域,而不是块作用域! 但是这经常悄悄地发生。
引入绑定
上面是 Ned Batchelder 在他的精彩演讲中 的一个特例,即以下所有语句都引入了一个新的局部变量 X:
X = ...
for X in ...
[... for X in ...]
(... for X in ...)
{... for X in ...}
class X(...):
def X(...):
def fn(X): ... ; fn(12)
with ... as X:
except ... as X:
import X
from ... import X
import ... as X
from ... import ... as X
(这些例子取自上面链接的演讲)
函数中的 import
引入了一个新的局部变量,该变量只能在该函数中访问! 在 Python 中导入不仅仅是告诉编译器在哪里找到一些代码,而是运行一些代码,将运行该代码的结果放入一个模块对象中,并创建一个新的局部变量来引用该对象。
细微的行为差异
赋值
Python =
就像 Java,它始终是一个引用,而不是一个副本(默认情况下在 C++ 中是副本)。
闭包
闭包是一个具有对外部作用域的引用的函数。 (mostly - read more) C++ 和 Java 都有类似的东西。 C++ 中的 Lambdas 要求非常精确地指定其绑定行为,因此每个变量可能按值或按引用或其他方式捕获。 因此,C++ 程序员至少会知道在 Python 中问这个问题,“这个变量是如何被捕获的?” 但是在 Java 中,默认行为是使捕获的变量最终确定,这有点可怕,因为 Java 程序员可能会对 Python 闭包做出同样的假设。
GC
它不一样! 在 Python 中,我们既有引用计数又有垃圾回收。 这使得它有点像 C++ 中的智能指针,有点像 Java 中的垃圾回收。 并且 __del__
finalizer 在 Python 2 中所做的与你想象的不同!
显式 super()
在 Java 和 C++ 中,存在对象的父构造函数为你调用的情况,但是在 Python 中,如果一个类覆盖了父类方法,则必须使用 super()
自己调用父方法实现。 Super 在 Python 中是一种非常协作的东西; 一个类可能在一个树中有很多超类,并且运行所有超类需要一个奇特的方法解析顺序。 这只有在每个类都调用 super 的情况下才有效。
我稍后会将此内容翻译成文字记录 - 现在你必须观看它,因为视觉效果很重要:explicit super。
无形的差异
属性和其他描述符
对于来自 C++ 或 Java 的人来说,我们不为 Python 中的 getters 和 setters 编写方法可能感觉很奇怪; 我们不必这样做,因为普通的属性 get 和 set 语法可以导致任意代码运行。
obj.attr
obj.attr = value
这属于无形类别,因为除非你进入该类的源代码,否则很容易假设像这样的代码只读取或写入一个变量。
动态属性查找
属性查找在 Python 中非常动态! 尤其是在编写测试和模拟行为时,重要的是要知道(例如)父类上的数据描述符将隐藏具有相同名称的实例变量。
Monkeypatching
替换类或实例上的实现对于人们来说将是新的。 它可能完全发生在程序的另一端(或你的测试套件中),但会影响你代码中的对象。
元编程
它在 Python 中占用更少的字符!
get_user_class("employee")("Tom", 1)
上面的代码基于字符串“employee”返回一个类对象,然后创建一个实例。 如果你期望元编程占用更多的代码行,那么可能很容易错过这一点。
Python 2 空格琐事
就解析重要的空格而言,在 Python 2 中,制表符是 8 个空格,但通常格式化为 4 个空格!
我们如何处理这些?
我们现在应该尝试教每个人所有这些东西吗? 也许不是! 如果有人感兴趣,当然可以。 但我认为如果没有太多上下文,很难击中所有这些。 并且要小心不要假设人们不知道这些事情,也许他们知道其中的 80%。 我认为这个速查表呈现了一些重要的事情,需要在教授任何其他在教学上最合适的主题时意识到。
我没有时间谈论太多关于教学的事情,所以我将指向 Sasha Laundy 的演讲(嵌入在上面),我喜欢它,并迅速 引用 Rose Ames 并说“知识就是力量; 它是以瓦为单位衡量的。” 我认为解决误解的一个好方法是向某人展示一个简短的代码示例 “wat”,该示例表明存在误解,而无需解释它,因为通常人们只需要指出他们模型中的缺陷即可。
代码审查是发送某人这样的 wat 的一个很好的动力。 我没有时间谈论代码审查,所以我将指向 Sandya Sankarram 关于它的这篇精彩文章。
我们可以使用此信息的另一件事是将其写在代码注释中。 我认为注释是解释代码为什么要做某事的地方,而不是解释该代码正在做什么。 但是如果你知道你的代码中发生的事情可能会让不太熟悉 Python 的人感到惊讶,也许你应该说出它在做什么? 或者也许你应该编写更简单的代码,而不是做那个有趣的 Python 特定事情。
以同样的方式,Python 库作者有时会编写跨越 Python 2 和 3 的代码,方法是在每个代码中表现相同,想象一下编写 Python 代码,如果它是 Java 或 C++,则会做同样的事情。 也许你会得到相当不地道的代码,但也许它会非常清楚。
image from this Stack Overflow blog post
Python 越来越受欢迎。 也许这意味着更多的人会理解它,我们将可以一直使用我们最喜欢的 Python 特定功能! 也许这意味着 Python 将成为通用语言,它应该尽可能简单明了。 我认为这将取决于代码库。 我认为随着代码库的增长,倾向于对不太了解 Python 的人来说不太令人惊讶的代码可能更有意义。
此速查表的最后一个用途是面试:面试是一种高压力的沟通练习,尝试预测另一个人对某件事的理解确实有帮助。 候选人经常用 Python 进行面试,但更了解 C++ 或 Java。 如果我可以识别一个误解,比如在类语句中初始化实例变量,我可以快速识别它,与候选人澄清,然后我们可以继续。 或者如果上下文足够清楚,我甚至不需要这样做。 当我在公司面试时,记住我可能需要向不太熟悉该语言的人解释我的 Python 代码的哪些部分很有帮助。
Tom Ballinger me@ballingt.com
想要评论吗?
github.com/thomasballinger twitter.com/ballingt
给我发送电子邮件或 pull request