Python 项目入门指南(三)
七、面向对象编程
许多语言被称为面向对象编程(OOP) 语言。Python、JavaScript、Java 和 C++只是使用 OOP 的几个名字。在这一周里,我们将开始理解 OOP 是什么,为什么它如此有用,以及如何在程序中实现它。
在 Python *(以及大多数语言)*中,我们通过自己构建的类来创建对象。您可以将类视为如何创建对象的蓝图。以第一人称射击游戏为例。所有的玩家、交通工具和武器都是物体。两个团队中可能各有五个人,但是这些人中的每一个都是从相同的蓝图中创建的。他们都有相似的特征,如体重、身高、发色等。你不用为十个不同的人写同样的代码,而是写一个蓝图,然后根据这个蓝图创造每个人。这压缩了代码,使程序更容易管理和维护。在周末,我们将一起构建一个完整的 21 点游戏,看看 Python 类的威力!
概述
-
理解面向对象编程的基础
-
什么和如何使用属性(类中的变量)
-
什么和如何使用方法(类中的函数)
-
理解继承的基础(父类或基类)
-
用类创建 21 点
挑战问题
以下代码的结果是什么?
>>> values = { 4:4, 8:8, "Q":10, "ACE":11 }
>>> card = ("Q", "Hearts")
>>> print("{ }".format(values[ card[ 0 ] ] ) )
星期一:创建和实例化一个类
Python 中的所有对象都是从类中创建的。OOP 的要点是重用相同的代码,同时灵活地创建每个具有自己特性的对象。今天,我们将学习 OOP 的术语和阶段,以及如何编写我们的第一个类。
为了继续今天的内容,让我们从“ python_bootcamp ”文件夹中打开 Jupyter 笔记本。打开后,创建一个新文件,并将其重命名为“ Week_07。"接下来,制作第一个标题为:"创建&实例化一个类的单元格。“我们将开始在那个牢房下面工作。
什么是对象?
看看你的周围,你看到了什么?可能有沙发、椅子、电视、书等。就在你身边。在编程中,所有这些都将作为对象被引用。甚至人也会被当作对象来引用。这是因为所有对象都来自特定的蓝图。在 Python 中,这些蓝图被称为类。让我们以汽车为例。所有的汽车都有相似的特征,可以用一个模板组装。每辆车通常都有车轮、颜色、品牌、型号、年份、车辆识别号等。类允许我们做的是构建一个包含所有这些特性的蓝图,并从中创建不同的汽车。这将减少我们必须编写的代码,并使我们能够为我们创建的任何汽车赋予特定于该对象的个人特征。图 7-1 说明了从同一个类创建多个对象的概念。
图 7-1
从相同的类蓝图创建三个相似的汽车
OOP 阶段
使用类时有两个阶段。第一阶段是类定义。像函数定义一样,这个阶段是您编写蓝图以供调用时使用的地方。第二阶段称为实例化。它是从类定义创建对象的过程。在一个对象被实例化后,它被称为一个实例。一个类定义中可能有多个实例。让我们开始看看如何定义一个类并创建一个实例!
创建一个类
使用类的第一步是创建类定义或"蓝图。创建一个新的类,语法类似于函数,但是你使用了类关键字而不是 def 。在这个类块的缩进中,我们将为我们的类属性和方法编写蓝图。不过现在不要担心这些;我们将在周二和周三复习这些内容。现在,我们将只使用关键字传递*。让我们来看一个例子:*
# creating your first class
class Car( ):
pass # simply using as a placeholder until we add more code tomorrow
去查查手机。什么也不会发生,但这很好,因为这意味着它的工作!所有的类都是用相同的结构创建的,除了不用写 pass ,我们将用赋予对象特性的代码填充这个块。
注意
在 Python 中,数据类型的基础也是类。打印出整数的类型导致。
创建实例
现在我们知道了如何创建类定义,我们可以开始理解如何创建一个对象的实例。像将数据类型存储到变量名中一样,我们使用类似的语法,除了在类名后面,我们使用括号。我们将在明天的课中复习这些括号的用途。让我们来看看:
# instantiating an object from a class
class Car( ): # parens are optional here
pass
ford = Car( ) # creates an instance of the Car class and stores into the variable ford
print(ford)
去查查手机。您将得到类似于 " < main 的输出。0x0332DB 处的汽车对象>。这描述了从“*汽车、*构建实例的类,以及类本身在内存中存储的位置“ 0x0332DB。“我们已经成功地创建了汽车对象的一个实例,并将其存储到变量“ ford ”中。
创建多个实例
请记住,您可以从每个类中创建任意数量的实例;但是,您将它们存储在单独的变量或数据集合中。让我们从我们的类中创建两个实例:
# instantiating multiple objects from the same class
class Car( ):
pass
ford = Car( )
subaru = Car( ) # creates another object from the car class
print( hash(ford) )
print( hash(subaru) ) # hash outputs a numerical representation of the location in memory for the variable
去查查手机。当我们输出变量的哈希值时,我们得到两个不同的数字。这些数字是变量在内存中位置的数字表示。这意味着尽管这两个变量是从同一个源创建的,但它们是作为独立的实体存储在程序中的。这就是物品的美,因为每个实例都有自己的特点。
周一练习
-
Animals :创建一个名为“Animals”的类,并从中创建两个实例。使用两个名为“狮子”和“老虎”的变量
-
解题:下面这段代码有什么问题?
>>> class Bus: >>> pass >>> school_bus = Bus( )
今天是进入面向对象编程世界的第一步。为了在 Python 中构建对象,我们必须首先创建类定义,也称为蓝图。从那里,我们可以从该类创建单个或多个实例。这个过程被称为实例化。明天我们将看到如何给每个实例赋予特性。
星期二:属性
昨天我们看到了如何创建一个类定义。今天,我们将开始了解如何给类和它们的实例赋予个性化的特性,即所谓的属性。属性只是定义在类中的变量,仅此而已。如果你听到有人在谈论属性,你会马上知道他们在谈论类。属性是我们如何存储每个对象实例的个人信息。将属性视为对象的信息来源。对于汽车,属性可以是颜色、车轮数量、座位数量、引擎大小等。
为了继续本课,让我们从之前的笔记本文件“ Week_07 ”继续,并在底部添加一个标有“属性”的 markdown 单元格。
声明和访问属性
像变量一样,我们用名称和值来声明属性;但是,它们是在类内部声明的。我们在上周已经讨论过范围。属性只在它们被定义的类中可用,所以为了访问一个属性,你必须创建一个实例:
# how to define a class attribute
class Car( ):
sound = "beep" # all car objects will have this sound attribute and its' value
color = "red" # all car objects will have this color attribute and its' value
ford = Car( )
print(ford.color) # known as 'dot syntax'
去吧,运行细胞。输出结果为“红色”。当我们从汽车类中实例化福特变量时,它是用两个属性创建的。这些属性是在类定义中自动设置的,因此从 Car 类创建的每个实例都将发出声音哔哔声,颜色为红色。“我们以后再看看如何改变这种情况。为了访问对象的属性,使用点语法。您可以从写实例的名称开始,后跟一个点和您想要访问的属性。所有类都使用类似的点语法来访问属性和方法(更多关于方法的内容参见)。
更改实例属性
不是所有你创建的对象都有相同的特征,所以你需要有能力改变属性值。为此,您需要使用点语法:
# changing the value of an attribute
class Car( ):
sound = "beep"
color = "red"
ford = Car( )
print(ford.sound) # will output 'beep'
ford.sound = "honk" # from now on the value of fords sound is honk, this does not affect other instances
print(ford.sound) # will output 'honk'
去查查手机。您会注意到,我们将在修改之前和之后输出福特实例的声音属性。使用点语法,我们能够给声音属性分配一个新值。这与改变变量的值没有什么不同。福特物体的声音属性现在将是**“嘎”**直到我们决定改变它。
使用 init()方法
到目前为止,我们一直在以非常基本的形式创建类。当你想实例化一个具有特定属性的对象时,需要使用初始化 ( init )方法。每当创建一个实例,就会立即调用 init 方法。您可以使用此方法在创建时实例化具有不同属性值的对象。这使我们能够轻松地创建具有个性化属性的类实例。现在,我们明天将讨论方法,所以不要太担心语法,但更要理解如何使用这个方法。这个方法的声明在单词 init 前后有两个下划线。它还将括号内的 "self" 关键字(在下一节中对此有更多介绍)作为强制参数。对于本例,我们将创建一个实例,并在实例化时定义颜色。让我们继续尝试一下:
1| # using the init method to give instances personalized attributes upon creation
3| class Car( ):
4| def __init__(self, color):
5| self.color = color # sets the attribute color to the value passed in
7| ford = Car("blue") # instantiating a Car class with the color blue
9| print(ford.color)
去查查手机。我们将得到一个结果输出**“蓝色**”。当我们创建福特实例时,它被初始化为属性颜色设置为蓝色。所有这些都发生在第 5 行。当我们声明要实例化的 ford 变量时,它立即将参数*“blue”传递给初始化方法。self 参数被忽略,并且“蓝色”被传入颜色参数。在 init 方法中,我们将 color 属性设置为刚刚传入的参数。因此值为“蓝色”*请记住,该方法的参数与函数的工作方式相同,并且需要按照正确的顺序。
“自我”关键词
self 关键字是对该类的当前实例的引用,用于访问与该实例相关联的变量和方法。想象一个你从未见过的足球队。你如何区分每个玩家和下一个玩家?你可能会用他们球衣背面的号码。即使每个玩家都有不同的特征,你也可以很容易地根据他们的号码挑选出他们中的任何一个。在 Python 中,本质上就是如何识别从同一来源创建的对象。在前一个单元格中,我们打印出了来自福特实例的属性颜色。Python 之所以知道在哪里访问这个值,特别是针对 ford ,是因为我们使用了 self 关键字。对于基本类,我们不需要它,因为这些属性是全局可访问的,这将在今天晚些时候讨论。现在,只要知道当您想要实例化一个具有个性化属性的对象时,您需要声明 init 方法,并使用 self 关键字来保存每个属性值。
用 init()实例化多个对象
为了真正理解 init 方法是如何工作的,让我们用两个不同值的属性实例化两个实例:
# defining different values for multiple instances
class Car( ):
def __init__(self, color, year):
self.color = color # sets the attribute color to the value passed in
self.year = year
ford = Car("blue", 2016) # create a car object with the color blue and year 2016
subaru = Car("red", 2018) # create a car object with the color red and year 2018
print(ford.color, ford.year)
print(subaru.color, subaru.year)
去查查手机。底部的两条 print 语句将输出每个实例的属性。当我们实例化福特和斯巴鲁对象时,我们为它们各自的属性赋予了不同的值。这就是 OOP 的妙处。我们能够用两行代码从同一个源构建两个不同的对象。即使类本身有几千行长,创建十个不同的实例也只需要十行代码。
全局属性与实例属性
不知不觉中,你已经使用了全局可访问的属性和实例可访问的属性。全局属性可以被类及其所有实例直接引用,而实例属性(,在 init 方法中定义)只能被类实例访问。如果一个属性是在一个类中声明的,而不是在 init 方法中声明的,那么它就是一个全局属性。使用 self 关键字在 init 方法中声明的任何属性都是实例属性。让我们看一个例子:
1| # using and accessing global class attributes
3| class Car( ):
4| sound = "beep" # global attribute, accessible through the class itself
6| def __init__(self, color):
7| self.color = "blue" # instance specific attribute, not accessible through the class itself
9| print(Car.sound)
11| # print(Car.color) won't work, as color is only available to instances of the Car class, not the class itself
13| ford = Car("blue")
15| print(ford.sound, ford.color) # color will work as this is an instance
去查查手机。在第 6 行,我们打印出声音**“beep”**,通过使用点语法的类蓝图直接访问它。您可以通过使用类名而不是实例名来实现这一点。我们能够这样做是因为声音属性被设置为一个全局可访问的属性。整个第 7 行被注释掉,因为它会产生一个错误,因为颜色属性是在 init 方法中声明的,并且只能被实例访问,而不能被类本身访问。最后,在第 9 行,在我们实例化了 ford 实例之后,我们打印出了声音和颜色属性。所有的类实例都可以访问全局和实例级别的属性,这就是为什么我们能够输出声音。然而,您必须记住的是,我们无法为 ford 实例的 sound 属性赋予个性化的值。只有在 init 方法中声明属性时,我们才能在实例化时赋予实例个人值。目前,为了给福特的声音属性赋予不同的值,我们必须在实例化后对其进行更改。
星期二练习
-
Dogs :创建一个Dogs类,它有一个全局属性和两个实例级属性。全局属性应该是“物种,值为“犬科动物”。"这两个实例属性应该是"名称和"*品种。*然后实例化两个狗对象,一个哈士奇名为萨米和一个巧克力实验室名为凯西。
-
用户输入:创建一个 Person 类,该类有一个名为的实例级属性。"要求用户输入他们的名字,并用他们输入的名字创建一个 Person 类的实例。然后打印出他们的名字。
今天我们学习了所有关于属性的知识,以及如何给类赋予个性化的变量。初始化方法和 self 关键字的使用允许我们在实例化时声明属性。最后,全局和实例级属性之间的差异是关键。初始化方法中的那些属性不能通过类直接访问,而是通过类的实例访问。
星期三:方法
当您想到对象时,您会将某些功能和动作与它们联系起来。以汽车为例。它们有颜色和轮子等属性,还有动作,如停止、加速、转弯等。在类中,这些动作被称为方法。方法本质上是类内的函数。如果你听到有人谈论方法,你会立刻知道他们在谈论 OOP。今天,我们将看到如何为我们的类声明方法,如何调用它们,以及它们为什么有用。
为了跟上这一课,让我们从笔记本文件“ Week_07 ”继续,并在底部添加一个标有“方法”的 markdown 单元格。
定义和调用方法
定义一个方法和定义一个函数是一样的;但是,您只需将代码放在类缩进块中。当声明一个想要通过实例访问的方法时,必须在定义中使用 self 参数。没有 self 关键字,方法只能由类本身访问。为了调用一个方法,你使用点语法。因为方法只是函数,所以调用它们时必须在实例名后加上括号:
# defining and calling our first class method
class Dog( ):
def makeSound(self):
print("bark")
sam = Dog( )
sam.makeSound( )
去查查手机。我们将得到“树皮作为我们的输出。当我们创建类定义时,它在蓝图中包含了方法 makeSound 。一旦我们创建了 Dog 类的实例,我们就能够通过使用点语法调用它来访问该方法。在一个类中,你可以有任意多的方法。
在方法中访问类属性
在您创建的方法中,您经常需要访问类中定义的属性。为此,您需要使用 self 关键字来访问该属性。记住 self 引用的是访问该类的实例。当我们创建多个实例时, self 允许程序理解返回哪个声音属性。即使对于全局属性也是如此。让我们看一个例子:
# using the self keyword to access attributes within class methods
class Dog( ):
sound = "bark"
def makeSound(self):
print(self.sound) # self required to access attributes defined in the class
sam = Dog( )
sam.makeSound( )
去查查手机。我们将再次得到“ bark 的输出,除了这一次,这是因为我们访问了在类中声明的声音属性。每当您需要使用 self 引用属性时,您必须在方法参数中包含 self 。
方法范围
像全局属性一样,您可能有可以通过类本身而不是类的实例访问的方法。这些也可以称为静 法。它们不能由类的实例访问。根据您构建的类,拥有一个只能通过类而不能通过实例访问的方法可能会有所帮助。让我们看一个例子:
1| # understanding which methods are accessible via the class itself and class instances
3| class Dog( ):
4| sound = "bark"
6| def makeSound(self):
7| print(self.sound)
9| def printInfo( ):
10| print("I am a dog.")
12| Dog.printInfo( ) # able to run printInfo method because it does not include self parameter
14| # Dog.makeSound( ) would produce error, self is in reference to instances only
16| sam = Dog( )
18| sam.makeSound( ) # able to access, self can reference the instance of sam
20| # sam.printInfo( ) will produce error, instances require the self parameter to access methods
去查查手机。这次我们在我们的 Dog 类中定义了两个方法。一个方法在参数中有 self ,而另一个没有。没有自身参数的方法可以通过类本身访问,这也是第 8 行输出“我是狗”的原因。”。第 9 行被注释掉是因为 makeSound 只能被我们的 Dog 类的实例访问,而不是类本身。最后,我们可以看到第 12 行也被注释掉了,因为没有用 self 作为参数定义的方法不能被类的实例访问。否则,我们会产生错误。这就是自我关键词的重要性。
将参数传递给方法
方法的工作方式与函数相同,可以将参数传递给要使用的方法。当传入这些参数时,不需要使用 self 参数引用它们,因为它们不是属性,而是该方法可以使用的临时变量:
# writing methods that accept parameters
class Dog( ):
def showAge(self, age):
print(age) # does not need self, age is referencing the parameter not an attribute
sam = Dog( )
sam.showAge( 6 ) # passing the integer 6 as an argument to the showAge method
去查查手机。我们会得到一个 6 的输出。在定义了一个狗的实例后,我们调用了方法 showAge ,并将整数 6 的参数传递给该方法。该方法然后能够打印出年龄。我们不需要说“ self.age ”,因为 self 引用的是类属性,而不是参数。
使用 Setters 和 Getters
在编程中有一个概念叫做设置器和获取器。它们是您创建来重新声明属性值并返回属性值的方法。我们已经看到了如何通过直接访问属性值来改变它们;然而,这有时会导致问题或意外改变值。好的做法是创建一个方法来改变属性值,并在需要设置新值时调用该方法。当您想要访问给定的属性值时,情况也是如此;不是直接访问它,而是调用一个将返回值的方法。这为我们提供了一种更安全的方法来访问实例属性。让我们看看如何:
1| # using methods to set or return attribute values, proper programming practice
3| class Dog( ):
4| name = ' ' # would normally use init method to declare, this is for testing purposes
6| def setName(self, new_name):
7| self.name = new_name # declares the new value for the name attribute
9| def getName(self):
10| return self.name # returns the value of the name attribute
11| sam = Dog( )
13| sam.setName("Sammi")
15| print( sam.getName( ) ) # prints the returned value of self.name
去查查手机。我们已经创建了两个方法,一个 setter 和一个 getter 。这些方法一般会有各自的关键字“ set ”和“ get ”开头的方法名。在第 4 行,我们定义了一个 setter 来接受一个 new_name 的参数,并将属性名更改为传入的值。这是更改属性值的更好的做法。在第 6 行,我们创建了一个 getter 方法,它简单地返回了 name 属性的值。这是检索属性值的更好的做法。第 9 行和第 10 行调用了这两个方法,以便修改并打印出返回值。
用方法增加属性
像 setters 一样,当您想通过增加或减少属性值而不是完全改变它时,最好的方法是创建一个方法来完成任务:
# incrementing/decrementing attribute values with methods, best programming practice
class Dog( ):
age = 5
def happyBirthday(self):
self.age += 1
sam = Dog( )
sam.happyBirthday( ) # calls method to increment value by one
print(sam.age) # better practice use getters, this is for testing purposes
去查查手机。对于这个例子,我们创建了一个名为 happyBirthday 的方法,它将在每次被调用时将狗的年龄加 1。这只是更好的实践,但不是更改类属性值的必需方法。
方法调用方法
当从另一个方法调用一个方法时,需要使用 self 参数。让我们创建一个 getter 方法和一个根据值打印出狗的信息的方法:
1| # calling a class method from another method
3| class Dog( ):
4| age = 6
6| def getAge(self):
7| return self.age
9| def printInfo(self):
10| if self.getAge( ) < 10: # need self to call other method for an instance
11| print("Puppy!")
13| sam = Dog( )
15| sam.printInfo( )
去查查手机。我们将在这里得到“ Puppy 的输出。我们可以从我们的 getter 中获得返回值,因为我们在 printInfo 方法中引用了 getAge 方法。它使用了 self 关键字和点语法。该条件被证明为真,因为返回值是 6 ,所以它继续在块内运行 print 语句。
神奇的方法
虽然它们有一个有趣的名字,魔法方法是 Python 中类的基础。不知不觉中,你已经使用了一个,初始化方法。所有魔法方法的名字前后都有两个下划线。当你打印任何东西的时候,你正在访问一个叫做 *str 的神奇方法。*当你使用操作符 (+,-,/,∑,==等。你正在使用魔法方法。它们本质上是决定 Python 中的操作符和其他任务执行什么的函数。不要太沉迷于它们,因为我们不会太多地使用它们,但我想向你介绍它们。如前所述,使用 print 函数时调用 str magic 方法;它代表一个类的字符串表示。让我们改变打印出我们自己定义的类时打印出的内容:
# using magic methods
class Dog( ):
def __str__(self):
return "This is a dog class"
sam = Dog( )
print(sam) # will print the return of the string magic method
去查查手机。以前当我们打印一个类时,它会输出类蓝图的名称和内存位置。现在,由于我们改变了 str 魔法方法,我们能够输出完全不同的打印语句。请记住, str 魔法方法期望返回一个字符串,而不是打印出来。所有神奇的方法都需要特定的参数和返回值。请随意查找更多的例子,并修改其他例子,看看它们是如何工作的!
周三练习
-
Animals :创建一个动物的类定义,该动物具有一个物种属性以及一个 setter 和 getter 来改变或访问属性值。创建一个名为“**狮子、**的实例,并使用参数“猫”调用 setter 方法。”然后通过调用 getter 方法打印出物种。
-
用户输入:创建一个类 Person ,它在实例化时接受一个名字,但是将年龄设置为 0。在类定义设置中,setter 和 getter 将要求用户输入他们的年龄,并将 age 属性设置为输入值。然后将格式化字符串中的信息输出为“你已经 64 岁了。“假设用户输入 64 作为他们的年龄。
今天,我们学习了方法以及它们在类中的基本功能。为了访问其他方法,我们需要使用 self 参数。方法为类提供了额外的功能,并且几乎在我们创建的每个类中都要用到。这将给给定类的所有实例相同的功能。
周四:继承
有时你会创建具有相似属性或方法的类。以一个关于狗和猫的课程为例。两者都有几乎相同的代码、属性和方法。我们使用一个叫做继承的概念,而不是写同样的代码两次。
为了跟上这一课,让我们从之前的笔记本文件“ Week_07 ”继续,并简单地在底部添加一个标有“继承”的单元格
什么是继承?
继承是允许类在编程中具有代码可重用性的概念之一。当你有两个或更多使用相似代码的类时,你通常想要建立一个所谓的“超类。“继承超类中所有代码的两个类被称为子类。“想到传承的一个好办法就是父母和他们的孩子。父母将基因传递给他们的孩子,这些基因是遗传的,并有助于确定孩子出生时的特征。继承以同样的方式工作,其中子类继承超类中的所有属性和方法。我们可以继承一个类,只需要编写一次代码,而不是为两个类编写两次相同的属性和方法。
继承一个类
要继承一个类,我们需要将我们继承的类的名称放在子类名称后面的括号中。让我们来试试:
1| # inheriting a class and accessing the inherited method
3| class Animal( ):
4| def makeSound(self):
5| print("roar")
7| class Dog(Animal): # inheriting Animal class
8| species = "Canine"
10| sam = Dog( )
12| sam.makeSound( ) # accessible through inheritance
14| lion = Animal( )
16| # lion.species not accessible, inheritance does not work backwards
去查查手机。在第 5 行,我们将动物类继承到我们的狗类中。这使得狗能够访问 makeSound 方法,这就是为什么在第 8 行,我们能够使用点语法来访问 makeSound 。但是请记住,继承不能向后工作,所以 Animal 不能访问 Dog 类中定义的属性和方法。因此,第 10 行被注释掉,因为物种属性在动物中不存在,试图访问它会产生错误。
使用 super()方法
使用继承时, super 方法用于创建向前兼容性。当声明超类中需要的属性时, super 用于初始化它的值。 super 的语法是关键字 super、括号、点号、初始化方法以及 init 调用括号内的任何属性。让我们看一个例子:
1| # using the super( ) method to declare inherited attributes
3| class Animal( ):
4| def __init__(self, species):
5| self.species = species
7| class Dog(Animal):
8| def __init__(self, species, name):
9| self.name = name
10| super( ).__init__(species) # using super to declare the species attribute defined in Animal
12| sam = Dog("Canine", "Sammi")
14| print(sam.species)
去查查手机。在第 6 行,我们声明 name 属性等于传入的参数,因为这个属性只在 Dog 类中定义。第 7 行是调用超级方法来初始化物种属性的地方,因为它是在超类动物内部声明的。这里使用 super 有助于减少代码行,这在超类需要几个属性时更明显。一旦调用了超级方法,我们的物种属性值被设置为传入的参数,我们现在可以通过我们的狗实例访问它,这就是为什么我们能够在第 9 行输出物种。
方法覆盖
有时当使用继承时,您希望子类能够在调用相同的方法时执行不同的操作。从之前创建的动物类中获取我们的 makeSound 方法。它打印出“咆哮”,但这不是你创建狗类时想要狗发出的声音。相反,我们使用方法覆盖的概念来改变方法的功能。在子类中,我们重新定义了方法(同名的*)来执行不同的任务。Python 总是首先使用子类中定义的方法,如果不存在,它将检查超类。让我们使用方法覆盖来改变 makeSound 方法,并为我们的 Dog 类打印正确的语句:*
1| # overriding methods defined in the superclass
3| class Animal( ):
4| def makeSound(self):
5| print("roar")
7| class Dog(Animal):
8| def makeSound(self):
9| print("bark")
11| sam, lion = Dog( ), Animal( ) # declaring multiple variables on a single line
13| sam.makeSound( ) # overriding will call the makeSound method in Dog
15| lion.makeSound( ) # no overriding occurs as Animal does not inherit anything
去查查手机。在第 8 行,我们声明了两个实例山姆和狮子。下一行是我们从我们的狗实例 sam 中调用 makeSound 方法的地方。由于方法覆盖,输出导致“吠声”。由于该方法被继承,但随后在 Dog 类中被重新定义,它打印 bark。在第 10 行,我们用我们的动物实例狮子调用同样的方法。这个输出是“ roar ,因为 lion 是 Animal 类的一个实例。请记住,继承不能反向工作。子类不能给超类任何特性。
继承多个类
到目前为止,我们已经看到了如何从单个超类继承。现在我们将尝试从多个类中继承。主要的区别是你如何超属性。不使用 super 方法,而是直接调用类名并传入带有属性的 self 参数。让我们看看如何:
1| # how to inherit multiple classes
3| class Physics( ):
4| gravity = 9.8
6| class Automobile( ):
7| def __init__(self, make, model, year):
8| self.make, self.model, self.year = make, model, year # declaring all attributes on one line
10| class Ford(Physics, Automobile): # able to access Physics and Automobile attributes and methods
11| def __init__(self, model, year):
12| Automobile.__init__(self, "Ford", model, year) # super does not work with multiple
14| truck = Ford("F-150", 2018)
16| print(truck.gravity, truck.make) # output both attributes
去查查手机。我们会得到 9.8 和福特的产量。在第 7 行,你会注意到我们继承了括号中的两个类福特类。第 9 行是这次奇迹发生的地方。我们不使用 super,而是通过直接调用继承类的名称来初始化变量。使用 init 方法,我们传递自身参数以及汽车需要的所有属性。Python 知道使用哪个超类,因为行首有名字。在最后一行,我们可以看到我们可以访问在 Physics 和 Automobile 中声明的两个属性,我们从这里继承。
周四练习
- 好人/坏人:创建三个类,一个名为“角色的超类,将使用以下属性和方法进行定义:
-
属性:姓名、队伍、身高、体重
-
方法:sayHello
The sayHello method should output the statement “Hello, my name is Max and I’m on the good guys”. The team attribute should be declared to a string of either “good” or “bad.” The other two classes, which will be subclasses, will be “GoodPlayers” and “BadPlayers.” Both classes will inherit “Characters” and super all the attributes that the superclass requires. The subclasses do not need any other methods or attributes. Instantiate one player on each team, and call the sayHello method for each. The output should result in the following:
>>> "Hello, my name is Max and I'm on the good guys" >>> "Hello, my name is Tony and I'm on the bad guys"
-
今天的主题是 OOP 中的继承。使用继承,我们可以减少我们在相似的类之间写的重复的行。继承的类称为超类,而执行继承的类称为子类。此外,重写继承方法的能力被称为方法重写,并为子类提供类定制。
星期五:创造 21 点
在这一周里,我们学习了如何使用 Python 中的类来改进我们的程序。今天,我们将把所有的知识放在一起,一起构建流行的游戏 21 点。我们将在整个程序中使用类,你将会看到我们如何用 Python 构建一个成熟的面向对象的游戏。假设你知道如何玩 21 点。如果没有,请随意查找规则和如何玩的步骤。
为了跟上这一课,让我们从之前的笔记本文件“ Week_07 ”继续,并在底部添加一个减价单元格,内容为“星期五项目:创造 21 点”。
完工图纸
和之前所有周五的项目一样,我们需要创建一个我们可以遵循的最终设计。本周有点不同,因为我们也需要首先设计我们的类。这将有助于我们在开始编程之前找出我们的类需要哪些属性和方法。坚持这个蓝图将会改进编程过程。首先,让我们想想我们需要什么类。在 21 点游戏中,你有特定的游戏规则、游戏动作和这副牌本身。然后我们还需要考虑有一个玩家和一个庄家在玩游戏。似乎我们需要创建两个类,一个用于游戏本身,一个用于两个玩家。你可能会争辩说,你需要一个单独的庄家和玩家类别;然而,我们保持这个游戏设计简单一点。我们先想想游戏类需要什么:
-
游戏属性
卡片组——容纳游戏中使用的所有 52 张卡片
花色–用于创建所有四种花色的牌组
值——用于创建一副牌,所有牌值的元组
-
游戏方式
制作一副牌——当被调用时创建新的 52 副牌
pull card——从一副牌中弹出任意一张牌并返还给它
游戏类主要是记录我们玩的牌。我们当然也可以将所有与游戏相关的方法放在这个类中;然而,我想让这些类简单一些,便于你理解。如果你想事后重构游戏,请随意。像 checkWinner,checkBust,handleTurn 等方法。,都可能是游戏类的一部分。在这一课中,我们不会担心将这些方法添加到游戏中。知道游戏类将要处理什么将帮助我们理解我们的玩家类需要什么。现在让我们继续计划这个类的属性和方法:
-
玩家属性
手牌——将牌储存在玩家手中
name–存储玩家或庄家名字的字符串变量
-
玩家方法
calcHand–返回计算所得的总点数
show hand——以格式优美的语句打印出玩家的手牌
add card——取一张牌并把它加到玩家的手上
正如我们所见,玩家类将记录每个玩家的手牌以及任何与改变手牌相关的方法。通常,您总是希望将改变属性的方法放在存储属性的同一个类中。现在我们已经对每个类所需的属性和方法有了一个很好的想法,我们将遵循这个指导方针来为游戏编程。
设置导入
让我们通过导入我们将使用的必要函数来开始编写这个程序:
1| # importing necessary functions
2| from random import randint # allows us to get a random number
3| from IPython.display import clear_output
随意测试一下 randint 函数。它接受两个参数,a 最小值和最大值,并将在这两个参数之间返回一个随机数。我们需要的另一个输入是清除笔记本单元输出的能力。
创建游戏类
接下来,我们将开始编写我们的主游戏类,我们称之为21 点。查看我们之前创建的设计,我们需要用属性牌组、花色和值初始化该类:
5| # create the blackjack class, which will hold all game methods and attributes
6| class Blackjack( ):
7| def __init__(self):
8| self.deck = [ ] # set to an empty list
9| self.suits = ("Spades", "Hearts", "Diamonds", "Clubs")
10| self.values = (2, 3, 4, 5, 6, 7, 8, 9, 10, "J", "Q", "K", "A")
我们将 deck 属性设置为空列表,因为我们将创建一个为我们创建 deck 的方法。另外两个属性被创建为元组,这样我们可以在不改变条目的情况下迭代它们。我们将使用它们来制作我们的卡片。
生成甲板
使用在二十一点类中定义的花色和值,我们将构建我们的牌组:
12| # create a method that creates a deck of 52 cards, each card should be a tuple with a value and suit
13| def makeDeck(self):
14| for suit in self.suits:
15| for value in self.values:
16| self.deck.append( (value, suit) ) # ex: (7, "Hearts")
18| game = Blackjack( )
19| game.makeDeck( )
20| print(game.deck) # remove this line after it prints out correctly
去查查手机。我们的 makeDeck 方法已经生成了 52 个元组的完整组,每个元组的值在 0 索引中,一个组在 1 索引中。我们将每张卡存储为一个元组,因为我们不想意外地改变值。在最后三行中,我们创建游戏的一个实例,调用 makeDeck 方法,并输出 deck 属性的值。完成后一定要删除最后一行,因为 print 语句仅用于调试目的。
从这副牌中抽出一张牌
现在我们已经创建了这副牌,我们可以创建一个方法来从这副牌中抽取一张牌。我们将使用 pop 方法,这样我们可以同时获得一个项目并将其从卡片组中移除:
16| self.deck.append( (value, suit) ) # ex: (7, "Hearts") ◽◽◽
18| # method to pop a card from deck using a random index value
19| def pullCard(self):
20| return self.deck.pop( randint(0, len(self.deck) – 1) )
22| game = Blackjack( )
23| game.makeDeck( )
25| print( game.pullCard( ), len(game.deck) ) # remove this line after it prints out correctly
去查查手机。您应该得到类似于“ (7,' Hearts') 51 ”的输出。元组是我们打印出来的牌,而 51 正在向我们证明它正在从这副牌中移除一张牌。我们设置了 pullCard 方法,这样它可以从一副牌中随机弹出一张牌。它随机选择,因为我们传递给 randint 的参数。我们允许的最大数量总是比卡片组的大小小一,因为索引是从零开始的。如果这副牌还有 45 张,我们希望随机整数在 0 到 44 之间。然后,它弹出随机索引中的项目,将其从卡片组中移除,并将其返回到调用该方法的位置。目前,我们只是将它打印出来,但稍后我们会将它添加到玩家的手中。完成后一定要删除最后一行,因为 print 语句仅用于调试目的。
创建玩家类
随着游戏类的正常运行,我们将注意力转向玩家类。让我们首先创建类定义来接受一个名称,并将指针设置为一个空列表:
20| return self.deck.pop( randint(0, len(self.deck) – 1) ) ◽◽◽
22| # create a class for the dealer and player objects
23| class Player( ):
24| def __init__(self, name):
25| self.name = name
26| self.hand = [ ]
28| game = Blackjack( )
29| game.makeDeck( )
31| name = input("What is your name?")
32| player = Player(name)
33| dealer = Player("Dealer")
34| print(player.name, dealer.name) # remove after working correctly
去查查手机。我们将得到输入的名称的打印声明,以及“经销商”。我们用名和手属性来定义要初始化的玩家类。 name 属性作为参数被接受,而 hand 被直接设置在类内部。在我们实例化了游戏对象后,我们要求用户输入他们的名字,并用他们的输入创建一个玩家类的实例。 dealer 对象将始终被称为“ Dealer ”,这就是为什么我们在实例化过程中使用传入的值来创建实例。
将牌添加到玩家手中
一旦我们正确实例化了 player 对象,我们就可以开始处理 Player 类所需的方法了。在看先用哪个方法编程的时候,总是需要思考哪些方法依赖于其他方法。对于这个类,卡尔昌德和 showHand 方法依赖于手中的牌。出于这个原因,我们将致力于 addCard 方法,然后关注另外两个:
26| self.hand = [ ] ◽◽◽
28| # take in a tuple and append it to the hand
29| def addCard(self, card):
30| self.hand.append(card)
32| game = Blackjack( ) ◽◽◽
37| dealer = Player("Dealer") ◽◽◽
39| # add two cards to the dealer and player hand
40| for i in range(2):
41| player.addCard( game.pullCard( ) )
42| dealer.addCard( game.pullCard( ) )
44| print( "Player Hand: { } \nDealer Hand: { }".format(player.hand, dealer.hand) ) # remove after
去查查手机。我们将在玩家的每手牌中随机获得两张牌的输出。 addCard 方法只是接受一个表示一张牌的元组,并将其附加到玩家的手上。在第 40 行,我们开始一个循环的*,这将为每手牌增加两张牌。它通过使用游戏实例方法 pullCard 来拉一张卡。该方法返回一个元组,然后该元组被传递给 addCard 方法,该方法随后被附加到相应玩家的手牌。这个循环将足以作为游戏的开始,其中所有玩家从他们手中的两张牌开始。确保删除最后一行,因为它用于调试。*
展示玩家的手牌
在前面的部分中,我们打印出了每个玩家的整手牌。然而,在实际的 21 点中,你只显示发给庄家的第二张牌。直接引用属性也是不好的做法,所以我们需要创建 showHand 方法来解决这两个问题。我们将使用格式良好的打印声明来显示牌,但更重要的是,我们将确保如果仍轮到玩家,那么您只能看到庄家的一张牌:
30| self.hand.append(card) ◽◽◽
32| # if not dealer's turn then only show one of his cards, otherwise show all
33| def showHand(self, dealer_start = True):
34| print( "\n{ }".format(self.name) )
35| print("===========")
37| for i in range( len(self.hand) ):
38| if self.name == "Dealer" and i == 0 and dealer_start:
39| print("- of –") # hide first card
40| else:
41| card = self.hand[ i ]
42| print( "{ } of { }".format( card[0], card[1] ) )
44| game = Blackjack( ) ◽◽◽
54| dealer.addCard( game.pullCard( ) ) ◽◽◽
56| # show both hands using method
57| player.showHand( )
58| dealer.showHand( )
去查查手机。输出的结果是玩家手里显示两张牌,而庄家只显示一张。让我们一步一步来。在第 33 行,我们用参数 dealer_start 声明了 showHand 方法。这个参数将是一个布尔值,它跟踪我们是否隐藏发牌者拿到的第一张牌。我们将默认值设置为 True ,这样我们唯一需要将参数 False 传入该方法的时间是在最后我们想要显示庄家的牌的时候。第 37 行的 for 循环允许我们打印出玩家对象手中的每张牌。第 38 行是我们检查两件事的地方:
-
调用这个方法的实例是经销商。
-
还没轮到庄家 (dealer_start == True)。
如果两者都为真,那么我们隐藏第一张牌;否则,我们将显示玩家和庄家的所有牌。声明卡片变量是为了在读取代码时易于使用,因为我们将它设置为我们手中的一个项目,它代表一张卡片。然后,我们打印带有元组值的格式化语句。这是通过访问代表每张卡的元组的 0 和 1 索引来完成的。在单元格的底部,我们为每个玩家对象调用这些方法。
计算手总数
既然我们能够调用一个方法来正确显示玩家的每手牌,我们需要计算这手牌中的牌的总数。然而,这种方法变得有点棘手,因为我们需要记住一些检查:
-
ace 可以值 11 或 1 分。如果总数超过 21,它们就值 1 分。
-
如果庄家只出示一张牌,那么即使他手里有两张牌,他手牌的价值也应该只代表这一张牌的价值。
-
所有的面牌(J,Q,K)都值 10 分。
有几种方法可以处理这个方法。我们一起编程只是众多方式中的一种。当考虑如何计算 ace 时,我们需要在计算完所有其他牌的总数后检查它们的值。我们会先记录我们有多少个 a,然后再合计。为了确保我们正确地返回庄家的总数,我们将像在 showHand 方法中那样跟踪是否轮到他。最后,为了计算牌面的值,我们将创建一个值字典来提取:
42| print( "{ } of { }".format( card[0], card[1] ) ) ◽◽◽
43| print( "Total = { }".format( self.calcHand(dealer_start) ) )
45| # if not dealer's turn then only give back total of second card
46| def calcHand(self, dealer_start = True):
47| total = 0
48| aces = 0 # calculate aces afterwards
49| card_values = {1:1, 2:2, 3:3, 4:4, 5:5, 6:6, 7:7, 8:8, 9:9, 10:10, "J":10, "Q":10, "K":10, "A":11}
51| if self.name == "Dealer" and dealer_start:
52| card = self.hand[ 1 ]
53| return card_values[ card[ 0 ] ]
55| for card in self.hand:
56| if card[ 0 ] == "A":
57| aces += 1
58| else:
59| total += card_values[ card[ 0 ] ]
61| for i in range(aces):
62| if total + 11 > 21:
63| total += 1
64| else:
65| total += 11
67| return total
69| game = Blackjack( ) ◽◽◽
去查查手机。从第 46 行开始,我们用参数 dealer_start 声明我们的 calcHand 方法。我们将这个参数设置为默认值 True ,这样它默认只显示庄家一张牌的总数。第 47 行是我们声明变量来跟踪总数的地方。第 48 行是我们声明变量的地方,用来记录我们手中有多少个 a。在第 49 行,我们声明了一个表示卡值的键值对字典。第 51 行的条件语句检查经销商实例是否是调用该方法的对象,以及经销商 _ 开始参数是否为真。如果它们都为真,那么我们将简单地返回庄家手中第二张牌的价值。这是第二张牌,因为我们将牌变量设置为等于手牌中的第二个项目,即第二张牌。然后我们用索引 0 中的卡片变量项引用 card_values 字典。这个条目将成为一个键,然后字典将返回这个键-值对的值。如果索引 0 处的项目是“ J ”,字典将返回值 10。从第 55 行开始的 for 循环将遍历相应玩家手中的每张牌,参考字典中的牌值,并将该牌值添加到当前总数中。如果这张牌是 a,它只会在我们的a 变量上加 1,而不会在总数上加任何东西。第 61 行的下一个 for 循环将循环玩家手中的 a 数。对于每张 ace,我们将根据总点数增加 1 点或 11 点。如果给这手牌加 11 分使总数大于 21,我们就简单地加一分。在方法结束时,我们返回总数和*。最后,第 43 行是我们在 showHand 方法中调用 calcHand 的地方。我们传递 dealer_start 变量,以防我们试图在庄家的回合中亮出这手牌。稍后,在庄家的回合中,我们将传递假的参数,然后它将计算所有庄家的牌的总数,而不仅仅是一张。*
处理玩家的回合
类定义现在已经完成了。我们可以开始关注主要的游戏流程。首先,我们将处理玩家的回合。他们应该有能力击中或者停留。如果他们留下,他们的回合就结束了。如果他们打中了,那么我们需要从一副牌中抽出一张牌,加到他们手上。卡片添加后,我们必须检查玩家是否超过 21 岁。如果他们这样做,他们就输了,我们需要跟踪这一点,以确定以后的输出:
83| dealer.showHand( ) ◽◽◽
85| player_bust = False # variable to keep track of player going over 21
87| while input("Would you like to stay or hit?").lower( ) != "stay":
88| clear_output( )
90| # pull card and put into player's hand
91| player.addCard( game.pullCard( ) )
93| # show both hands using method
94| player.showHand( )
95| dealer.showHand( )
97| # check if over 21
98| if player.calcHand( ) > 21:
99| player_bust = True # player busted, keep track for later
100| print("You lose!") # remove after running correctly
101| break # break out of the player's loop
去查查手机。现在,试着打到 21 岁以上。这将导致输出"你输了!”。如果你不超过 21 岁,什么都不会发生,因为我们还没有解决这个问题,但是我们会到达那里的。在第 85 行,我们声明了一个变量来跟踪超过 21 岁的玩家。然后我们开始 while 循环,询问用户是想打还是留下。如果他们选择留下,那么这个循环将会继续。在循环中,我们将清除输出,向玩家的手牌中添加一张牌,展示这手牌,然后检查他们是否失败。这个循环有两种结束方式,要么破产,要么选择留下。
处理庄家的回合
庄家的回合将与玩家的回合非常相似,但是我们不需要问庄家是否愿意打。庄家在 17 岁以下自动击球。我们还需要追踪经销商是否破产:
100| break # break out of the player's loop ◽◽◽
102| # handling the dealer's turn, only run if player didn't bust
103| dealer_bust = False
105| if not player_bust:
106| while dealer.calcHand(False) < 17: # pass False to calculate all cards
107| # pull card and put into player's hand
108| dealer.addCard( game.pullCard( ) )
110| # check if over 21
111| if dealer.calcHand(False) > 21: # pass False to calculate all cards
112| dealer_bust = True
113| print("You win!") # remove after running correctly
114| break # break out of the dealer's loop
去查查手机。尝试运行单元格,直到庄家超过 21,导致打印语句运行。我们首先在第 103 行声明一个变量来跟踪破产的经销商。在第 105 行,我们检查玩家是否已经被终结,因为这一轮已经结束,庄家不需要抽任何牌。第 106 行是我们循环的起点,它将向庄家的手牌中添加一张牌,并检查他是否失败。循环将继续,直到庄家超过 16 点,或者他超过 21 点。当我们这次为庄家调用 calcHand 方法时,我们传递了 False 的参数。这样一来,该方法将计算整手牌的总数,而不仅仅是第二张牌,就像我们之前所做的那样。
计算赢家
这个游戏的最后一步是计算谁是赢家。到目前为止,我们已经做了一些检查,看看玩家是否已经输了 21 分。我们将首先检查玩家是否破产,然后是庄家。如果两个玩家都没有失败,那么我们需要看看谁的总得分更高。如果他们打成平手,那么这就是所谓的推,没有人会赢:
113| break # break out of the dealer's loop ◽◽◽
115| clear_output( )
117| # show both hands using method
118| player.showHand( )
119| dealer.showHand(False) # pass False to calculate and show all cards, even when there are 2
121| # calculate a winner
122| if player_bust:
123| print("You busted, better luck next time!")
124| elif dealer_bust:
125| print("The dealer busted, you win!")
126| elif dealer.calcHand(False) > player.calcHand( ):
127| print("Dealer has higher cards, you lose!")
128| elif dealer.calcHand(False) < player.calcHand( ):
129| print("You beat the dealer! Congrats!")
130| else:
131| print("You pushed, no one wins!")
去查查手机。我们现在有一个功能齐全的 21 点游戏!首先,我们清除输出并显示玩家的双手。不过,主要的区别在第 119 行。我们将参数 False 传递给庄家的 showHand 方法。这是为了显示所有庄家的牌,以及完整的总数。记得我们在 showHand 中调用了 calcHand 方法,并传递了 dealer_start 的值,我们通过这个方法调用将该值设置为 False 。之后,我们设置一些条件,根据给定的条件输出正确的结果。
最终输出
祝贺您完成这个项目!由于项目的规模,你可以在 Github 上找到完整版本的代码。要找到这个项目的具体代码,只需打开或下载“ Week_07.ipynb 文件。如果您在这个过程中遇到了错误,请确保将您的代码与该文件中的代码进行交叉引用,看看您可能在哪里出错了。
尽管今天的项目很长,但我们还是看到了一些面向对象编程的好例子。使用类给了我们重用几行代码的能力,就像我们对玩家和庄家对象所做的那样。这个程序当然可以重构,在 21 点类中有更多的方法;然而,我希望您能够更容易地阅读代码。出于这个原因,我保持了较短的类和独立的主要游戏功能。如果你愿意的话,一定要测试这个游戏并加入你自己的特色。
每周总结
在这一周里,我们讨论了面向对象编程的概念,以及为什么它们在编程世界中如此重要。在 Python 中,我们称它们为类。它们允许我们重用代码并从一个对象创建多个实例。当在类中存储变量或创建函数时,它们被称为属性和方法。我们可以使用点语法和 self 参数来引用它们。如果没有类,我们将需要为程序中的所有对象硬编码每一行。这在大型项目中变得尤为明显。为了增加代码的可重用性,我们可以使用继承。这允许子类从超类继承属性和方法,就像父类和子类一样。在这个周末,我们能够创建一个面向对象的 21 点游戏。这展示了 OOP 的能力,因为我们能够创建 player 对象的多个实例。向前看,一定要把你周围的世界当成物体。它将帮助你适应面向对象的世界,理解对象的属性和方法。
挑战问题解决方案
挑战题的解答是 10 。这个输出背后的原因是字典的工作方式。记住,当从字典中访问信息时,您可以访问键-值对。当从字典中访问一个键时,您会得到那个键-值对的值。下面一行正在访问 card 变量中第一项的值:
>>> card[0]
这将导致**“Q”,因为它是分配给卡的元组中的第一个项目。当我们访问字典时,我们正在访问“Q”**键的值。最后一行应该是这样的:
>>> print("{ }".format(values["Q"]))
这将输出**“Q:10”**键-值对的值,即 10 。
每周挑战
要测试你的技能,请尝试以下挑战:
-
游戏循环:使用我们周五项目中的代码,创建一个游戏循环,这样你就可以不断地玩新的一手牌,直到玩家决定退出。只有当玩家输入“退出”时,电池才会停止运行;否则,你应该继续玩新牌。
-
添加货币:使用我们周五项目中的代码,在游戏中添加下注货币的功能。确保跟踪玩家类中的货币,因为属性应该属于那个对象。在每手牌之前,询问用户他们想下多少注;如果他们赢了,将该金额加入他们的货币;如果他们输了,从他们现在拥有的中减去这个数额;如果他们打成平手,什么也不会发生。
八、高级主题 I:效率
现在我们有了一个坚实的基础,我们可以开始深入更高级的主题。在接下来的两周里,我们将讨论一些概念,这些概念有助于减少您需要编写的代码量。这些概念中的许多将帮助我们为第 10 周的数据分析做准备。
本周,我们将讨论使用列表理解和匿名函数的一行程序。这将有助于通过在一行中压缩相同的功能来减少代码行。然后,我们将介绍一些内置的 Python 函数,这些函数使数据处理变得更加容易。我们讨论的最后一个概念是当函数调用自己时,称为递归函数。通常,这些类型的函数缺乏效率,所以我们将介绍如何使用一个叫做记忆化的缓存概念。由于本周都是关于高级主题,我们将深入到编程中更重要的算法之一… 二分搜索法!我们将看到如何一行一行地编写这个算法,并理解搜索算法如何有效地工作。
概述
-
使用理解在一行中构建列表
-
理解一行匿名函数
-
使用 Python 的内置函数进行列表变更
-
理解递归函数以及如何改进它们
-
为二分搜索法编写算法
挑战问题
在本周的挑战中,我想让你们编写一个程序,让一个用户输入一个数字,然后告诉这个用户他们输入的数字是不是一个质数。记住,质数只能被 1 和它本身整除,并且必须在数字 2 之上。创建一个名为“ isPrime 的函数,将输入传递给它,并返回一个真或假值。在对该功能进行编程时,请务必牢记效率。
周一:列表理解
列表理解允许我们创建一个在单行中填充数据的列表。我们不用创建一个空列表,遍历一些数据,并在单独的行上把它附加到列表中,我们可以使用理解一次执行所有这些步骤。它不会提高性能,但是它更干净,有助于减少程序中的代码行。有了理解,我们可以把两行或更多行简化成一行。另外,一般来说写起来更快。
为了继续今天的内容,让我们从“ python_bootcamp ”文件夹中打开 Jupyter 笔记本。打开后,创建一个新文件,并将其重命名为“ Week_08。“接下来,对标题为:“列出理解”的第一个单元格进行降价。“我们将开始在那个牢房下面工作。
列表理解语法
使用列表理解的语法取决于你要写的内容。列表理解的一般语法结构如下所示:
>>> *result* = [ *transform* *iteration* *filter* ]
例如,当您想要填充列表时,语法应该具有以下结构:
>>> name_of_list = [ item_to_append for item in list ]
但是,当您想要包含 if 语句时,理解应该如下所示:
>>> name_of_list = [ item_to_append for item in list if condition ]
只有满足条件,项目才会被追加到新列表中;否则不会收录。最后,如果您想包含一个 else 条件,它将如下所示:
>>> name_of_list = [ item_to_append if condition else item_to_append for item in list ]
当在列表理解中使用 else 条件时,只有当 if 语句证明为真时,第一项才会被追加到列表中。如果为 False,则 else 语句后面的项将被追加到列表中。
生成数字列表
让我们尝试使用列表理解生成一个从 0 到 100 的数字列表:
# create a list of ten numbers using list comprehension
nums = [ x for x in range(100) ] # generates a list from 0 up to 100
print(nums)
去查查手机。您会注意到我们输出了一个包含 100 个数字的列表。列表理解允许我们在一行中构建这个列表,而不是在单独的行中写出 for 循环和 append 语句。来自前面单元格的理解是以下代码的精确表示:
>>> nums = [ ]
>>> for x in range(100):
>>> nums.append(x)
如你所见,我们使用理解将三行减少到一行。这不会提高性能,但会减少代码中的行数。这在更大的程序中变得更加明显,我强烈建议你尽可能地使用理解。接下来,我们将开始在建立列表时使用列表理解。
If 语句
前面,我们讨论了在你的理解中包含一个 if 语句时,语法是如何变化的。让我们试一个例子,只列出偶数:
# using if statements within list comprehesion
nums = [ x for x in range(10) if x % 2 == 0 ] # generates a list of even numbers up to 10
print(nums)
去查查手机。出于这种理解,只有当条件证明为真时,变量 x 才会被追加到列表中。在我们的例子中,当 x 的当前值被 2 整除时,条件为真。在下面的内容中,您会发现不使用理解时所需的相同代码:
>>> nums = [ ]
>>> for x in range(10):
>>> if x % 2 == 0:
>>> nums.append(x)
这一次,我们能够将四行代码减少到一行。这通常可以提高代码的可读性。
If-Else 语句
现在让我们更进一步,添加一个 else 语句。这一次,当数字被 2 整除时,我们将追加字符串“偶数”;否则,我们将追加字符串“奇数”:
# using if/else statements within list comprehension
nums = [ "Even" if x % 2 == 0 else "Odd" for x in range(10) ] # generates a list of even/odd strings
print(nums)
去查查手机。这将输出一个表示奇数或偶数的字符串列表。这里,当 if 条件为真时,我们追加字符串“ Even ”;否则,else 语句将被命中并追加字符串“奇数”。没有理解的代码的相同表示可以在下面找到:
>>> nums = [ ]
>>> for x in range(10):
>>> if x % 2 == 0:
>>> nums.append("Even")
>>> else:
>>> nums.append("Odd")
我们已经将代码行从六行减少到一行。理解对于快速生成数据非常重要;但是条件大了就变得更难了。理解不允许使用 elif 语句,只允许使用 if/else 语句。
用变量列表理解
Comprehension 对于从其他列表中生成数据也非常有用。让我们利用理解力,得到一个数字列表,并生成这些数字的平方的单独列表:
# creating a list of squared numbers from another list of numbers using list comprehension
nums = [2, 4, 6, 8]
squared_nums = [ num**2 for num in nums ] # creates a new list of squared numbers based on nums
print(nums)
去查查手机。我们会得到**【4,16,36,64】**的输出。对于这个例子,我们能够通过添加表达式“num∫2”来生成平方数。没有理解的代码的相同表示如下所示:
>>> squared_nums = [ ]
>>> for num in nums:
>>> squared_nums.append(num**2)
在本例中,我们能够将所需的线路从三条减少到一条。
词典理解
不仅可以在列表上使用理解力,还可以在 Python 字典上使用理解力。语法结构完全相同,只是需要包含一个键-值对而不是一个数字来插入到字典中。让我们创建一个偶数作为键的字典,其中值是键的平方:
# creating a dictionary of even numbers and square values using comprehension
numbers = [ x for x in range(10) ]
squares = { num : num**2 for num in numbers if num % 2 == 0 }
print(squares)
去查查手机。我们会得到如下结果:“ {0: 0,2: 4,4: 16,6: 36,8: 64} ”。我们能够使用 comprehension 添加每个键值对,同时使用条件语句检查它们是否是偶数。
周一练习
-
度转换:利用列表理解,将下面的列表转换为华氏。目前,温度单位是摄氏度。换算公式为“ (9/5) * C + 32 ”。你的输出应该是**【53.6,69.8,59,89.6】。**
>>> degrees = [ 12, 21, 15, 32 ] -
用户输入:要求用户输入一个 100 以内的整数。使用列表理解生成一个能被 100 整除的数字列表。例如,如果输入了数字 25,那么输出应该是**【25,50,75,100】**。
今天的重点是使用一个叫做列表理解的概念来生成列表。根据需要的表达式,您将使用特定的语法结构。理解并不能提高表现;相反,它减少了执行相同任务所需的代码行数。它还可以提高可读性。
星期二:λ函数
Lambda 函数,也称为匿名函数,是 Python 中的单行函数。像列表理解一样,lambda 函数允许我们减少程序中需要编写的代码行。它不适用于复杂的函数,但有助于提高较小函数的可读性。
为了跟上这一课,让我们从之前的笔记本文件“ Week_08 ”继续,并简单地在底部添加一个标有“ Lambda 函数”的 markdown 单元格。
Lambda 函数语法
lambda 函数的语法通常保持不变,不像开始添加条件语句时的列表理解。首先,让我们看看基本结构:
>>> lambda arguments : expression
Lambdas 总是以关键字 lambda 开头。接下来你会发现任何被传入的参数。在冒号的右边,我们将看到要执行并返回的表达式。Lambdas 默认返回表达式,所以我们不需要使用关键字:
>>> lambda arguments : value_to_return if condition else value_to_return
像列表理解一样,条件语句放在最后。这是 lambda 函数最复杂的部分。超过这一点就需要完整地写出函数。
注意
Lambdas 基本上在冒号右边使用三元运算符。
使用λ
当使用 lambdas 而不将它们存储到变量中时,需要用圆括号将函数和任何传入的参数括起来。让我们从小处着手,编写一个 lambda 函数,它将返回参数平方的结果:
# using a lambda to square a number
( lambda x : x**2 )( 4 ) # takes in 4 and returns the number squared
去查查手机。我们将得到一个 16 的输出。第一组括号包含 lambda 函数。第二组保存传入的参数。在这种情况下,整数 4 被传入 x,并执行表达式x∫2并返回结果。它们被称为匿名函数,因为它们没有名字。在下面的内容中,您将找到为执行相同执行的普通函数编写的代码:
>>> def square(x):
>>> return x**2
>>> square(4)
我们把三条线变成了一条。一旦你习惯了阅读 lambda 语法,用这些函数来读写程序就变得更容易了。
传递多个参数
Lambdas 可以接受任意数量的参数,比如函数。让我们这次尝试传入两个参数,并将它们相乘:
# passing multiple arguments into a lambda
( lambda x, y : x * y )( 10, 5 ) # x = 10, y = 5 and returns the result of 5 * 10
去查查手机。我们将得到 50 的产量。这一次 lambda 函数在冒号的左边接受了两个参数 x 和 y。在冒号的右边,它能够执行将这两个参数相乘并返回结果的表达式。在下面,你会发现相同的代码,就像我们写了一个普通的函数一样:
>>> def multiply(x, y):
>>> return x * y
>>> multiply(10, 5)
和以前一样,我们能够保存几行代码来获得相同的结果。
保存 Lambda 函数
Lambdas 之所以命名为匿名函数,是因为它们没有名称来引用或调用。lambda 函数一旦被使用,除非保存到变量中,否则不能再次使用。让我们像以前一样使用同一个 lambda 函数,只是这次将它保存到一个名为" square 的变量中,即使在 lambda 函数被读取后也可以引用该变量:
# saving a lambda function into a variable
square = lambda x, y : x * y
print(square)
result = square(10, 5) # calls the lambda function stored in the square variable and returns 5 * 10
print(result)
去查查手机。我们将得到和以前一样的输出,除了这次我们通过调用函数 square 得到它。当函数存储在变量内部时,变量名充当函数调用。当我们在 square 变量中存储一个 lambda 时,我们能够通过调用 square 并传递参数来调用 lambda 函数。
注意
即使是正常定义的函数也可以保存到变量中,并通过变量名引用。
条件语句
一旦开始向 lambda 函数中添加条件语句,它们的行为方式与三元运算符相同。唯一的区别是您必须同时提供 if 和 else 语句。你不能只使用 if 语句;这将导致语法错误,因为它总是需要一个表达式来返回。让我们创建一个 lambda,它将返回传入的两个参数之间的较大值:
# using if/else statements within a lambda to return the greater number
greater = lambda x, y : x if x > y else y
result = greater(5, 10)
print(result)
去查查手机。我们将得到一个 10 的输出,因为它是较高的值。当你需要一个可以执行简单条件的函数时,Lambdas 非常有用。与普通函数相同的代码如下所示:
>>> def greater(x, y):
>>> if x > y:
>>> return x
>>> else:
>>> return y
>>> result = greater(5, 10)
当使用条件语句时,很容易看到 lambda 函数的威力。在这种情况下,我们能够将五行代码变成一行。
返回 Lambda
lambda 函数的亮点在于它们能够使其他函数更加模块化。假设我们有一个函数,它接受一个参数,我们希望这个参数在程序的后面与一个未知数相乘。我们可以简单地创建一个变量来存储返回的 lambda 函数,同时传递一个参数。让我们试几个例子:
# returning a lambda function from another function
def my_func(n):
return lambda x : x * n
doubler = my_func(2) # returns equivalent of lambda x : x * 2
print( doubler(5) ) # will output 10
tripler = my_func(3) # returns equivalent of lambda x : x * 3
print( tripler(5) ) # will output 15
去查查手机。我们将得到一个输出 10 和 15 。当我们定义我们的 doubler 变量时,我们调用 my_func ,同时传入整数值 2 。该值在 lambda 函数中使用,然后返回 lambda。然而,lambda 并不是以"lambda x:x∫n"的形式返回;现在返回的是整数 2 而不是 n 。每当调用 doubler 时,它实际上是被调用的 lambda 函数。这就是为什么当我们将值 5 传入倍增器时,我们会得到 10 的输出。这同样适用于我们的变量三倍器。由于返回的 lambda 函数,我们能够修改 my_func 的结果。
星期二练习
-
填空:为下面的代码填空,使其接受一个参数“x”,如果大于 50 则返回“True”;否则,它应该返回“False”:
>>> ____ x _ True if x _ 50 ____ False -
度数转换:编写一个 lambda 函数,它接受摄氏度的度数值,并返回转换成华氏度的度数。
今天,我们能够理解普通函数和匿名函数之间的区别,或者称为 lambda 函数。它们有助于提高可读性,并能够精简您的代码。它们最强大的特性之一是能够通过从函数返回来赋予函数更多的功能。
星期三:映射、过滤和减少
当处理数据时,通常需要能够从数据中修改、过滤或计算表达式。这就是这些重要的内置函数发挥作用的地方。 map 函数用于迭代数据集合并修改它。过滤器函数用于迭代一个数据集合,你猜对了…过滤掉不符合条件的数据。最后, reduce 函数获取一个数据集合并将其压缩成一个结果,就像列表的 sum 函数一样。
为了跟上这一课,让我们从笔记本文件“ Week_08 ”继续,简单地在底部添加一个 markdown 单元格,表示“映射、减少和过滤”。
不带 Lambdas 的地图
当您需要改变一个可迭代数据集合中的所有项目时,使用 map 函数。它接受两个参数,应用于每个元素的函数和可迭代数据。使用 map 时,返回一个 map 对象,它是一个迭代器。现在不要担心这些是为了什么;只要知道我们可以把它们转换成我们可以处理的数据类型,比如列表。让我们尝试获取一个摄氏温度列表,并将其全部转换为华氏温度:
1| # using the map function without lambdas
2| def convertDeg(C):
3| return (9/5) * C + 32
4| temps = [ 12.5, 13.6, 15, 9.2 ]
5| converted_temps = map(convertDeg, temps) # returns map object
6| print(converted_temps)
7| converted_temps = list(converted_temps) # type convert map object into list of converted temps
8| print(converted_temps)
去查查手机。第一个 print 语句将在 0x 0dc 3d 3>输出“ < map object”或类似的内容。这是因为 map 函数返回的是 map 对象,而不是转换后的数据集合。在第 7 行,我们能够将地图对象转换成一个列表,结果是输出“【54.5,56.48,59,48.56】”。当映射被调用时,该函数开始迭代传入的 temps 列表。在迭代过程中,它将单个项目传递给 convertDeg 函数,直到传递完所有项目。该过程的等效过程如下:
>>> for item in temps:
>>> convertDeg(item)
转换后,它会将数据追加到地图对象中。直到我们转换了地图对象,我们才能看到转换后的温度。
带有 Lambdas 的地图
既然我们已经看到了如何将 map 与一个正常定义的函数一起使用,那么这次让我们尝试一下 lambda 函数。由于 map 需要一个函数作为第一个参数,我们可以简单地用 lambda 代替已定义函数的名称。我们也可以在同一行键入 convert:
# using a map function with lambdas
temps = [ 12.5, 13.6, 15, 9.2 ]
converted_temps = list( map( lambda C : (9/5) * C + 32, temps) ) # type convert the map object right away
print(converted_temps)
去查查手机。我们将得到和以前一样的输出,但是代码行要少得多。这就是结合这两个概念的妙处。当 map 函数遍历 temps 列表并返回转换后的值时,lambda 函数接受每个项目。我们正在执行的相同过程可以在下面的代码行中找到:
>>> def convertDeg(degrees):
>>> converted = [ ]
>>> for degree in degrees:
>>> result = (9/5) * degree + 32
>>> converted.append(result)
>>> return converted
>>> temps = [ 12.5, 13.6, 15, 9.2 ]
>>> converted_temps = convertDeg(temps)
>>> print(converted_temps)
如您所见,lambda 函数和 map 的使用有助于在我们需要更改数据时减少代码行数。
不带 Lambdas 的过滤器
过滤器功能对于收集数据和删除任何不需要的信息非常有用。像映射函数一样,它接受一个函数和一个可迭代的数据类型,并返回一个过滤器对象。这个对象可以被转换成一个工作列表,就像我们对 map 对象所做的那样。让我们使用相同的数据,过滤掉不超过 55 华氏度的温度:
# using the filter function without lambda functions, filter out temps below 55F
def filterTemps(C):
converted = (9/5) * C + 32
return True if converted > 55 else False # use ternary operator
temps = [ 12.5, 13.6, 15, 9.2 ]
filtered_temps = filter(filterTemps, temps) # returns filter object
print(filtered_temps)
filtered_temps = list(filtered_temps) # convert filter object to list of filtered data
print(filtered_temps)
去查查手机。第一次输出的结果是“ <过滤器对象在 0x 0dc 3d 3>”,就像我们的地图对象输出。第二条语句的结果是“【56.48,59】”的输出。当我们使用过滤器并传入 temps 时,它一次遍历列表中的一项。然后,它会将每个项目传递给 filterTemps 函数,无论返回的结果是真还是**假,**它都会将项目添加到过滤器对象中。直到我们把对象转换成一个列表,我们才能输出数据。使用 lambda 函数可以进一步减少所需的代码行。
使用 Lambdas 进行过滤
让我们执行与前面相同的步骤,只是这次我们将使用 lambda 函数:
# using the filter function with lambda functions, filter out temps below 55F
temps = [ 12.5, 13.6, 15, 9.2 ]
filtered_temps = list( filter( lambda C : True if (9/5) * C + 32 > 55 else False, temps) ) # type convert the filter
print(filtered_temps)
去查查手机。我们将得到与前面相同的输出,只是这次我们能够减少 lambda 函数使用的行数。我们正在执行的相同过程可以在下面的代码行中找到:
>>> def convertDeg(degrees):
>>> filtered = [ ]
>>> for degree in degrees:
>>> result = (9/5) * degree + 32
>>> if result > 55:
>>> filtered.append(degree)
>>> return filtered
>>> temps = [ 12.5, 13.6, 15, 9.2 ]
>>> filtered_temps = convertDeg(temps)
>>> print(filtered_temps)
就像使用 lambdas 的 map 函数一样,将 filter 函数与 lambda 结合起来可以大大减少代码。
Reduce 的问题是
尽管我将向您展示如何使用 reduce 函数,但您应该明白,有一种比使用实际函数更好的方法。据 Python 的创造者自己说:
所以现在减少()。这实际上是我最讨厌的一个,因为除了几个涉及+或的例子之外,几乎每次我看到带有重要函数参数的 reduce()调用,我都需要拿起笔和纸,在我理解 reduce()应该做什么之前,画出实际输入到该函数中的内容。因此,在我看来,reduce()的适用性仅限于关联操作符,在所有其他情况下,最好显式写出累加循环。* 1
用他自己的话来说,他是说 reduce 只服务于几个目的,但除此之外,它是没有用的,所以对循环使用一个简单的更有意义。让我们看看这两个例子。
注意
Reduce 是 Python 2 中的一个内置函数,从那以后,它被移到了 functools 库中。
使用 Reduce
reduce 函数接受两个参数,执行的函数和迭代的数据集合。但是,与过滤器和贴图不同,reduce 一次迭代两个项目,而不是一个项目。reduce 的结果是总是返回一个结果。在下面的例子中,我们想把所有的数字相乘。让我们用 reduce 来执行这个例子:
# for informational purposes this is how you use the reduce function
from functools import reduce
nums = [ 1, 2, 3, 4 ]
result = reduce( lambda a, b : a * b, nums ) # result is 24
print(result)
去查查手机。输出将是 24。由于 reduce 函数接受两个参数,它将 nums 列表压缩为一个返回值。在下文中,您将看到执行相同过程的建议方式:
>>> total = 0
>>> for n in nums:
>>> total = total * n
在很大程度上,很容易理解为什么 Rossum 如此坚持建议用 for loops 来代替,因为当你尝试更复杂的数据集合,如列表中的列表时, reduce 会变得难以理解。
周三练习
-
映射名字:使用 lambda 和 map 函数映射下面的名字列表,产生下面的结果" [ "Ryan "," Paul "," Kevin Connors" ] 。
>>> names = [ " ryan", "PAUL", "kevin connors " ] -
Filter Names :使用 lambda and filter 函数,过滤掉所有以字母“a”开头的名字,使其不区分大小写,因此它会过滤掉名字的大小写。下面列表的输出应该是 ["弗兰克","里帕尔"] 。
>>> names = [ "Amanda", "Frank", "abby", "Ripal", "Adam" ]
今天,我们学习了一些重要的内置函数,在 Python 中处理数据时可以使用这些函数。将映射和过滤器与 lambdas 结合起来有助于提高我们的代码可读性,并缩短所需的代码行。最后,减少在一些情况下会有帮助;然而,for 循环通常更具可读性。
星期四:递归函数和记忆
递归是编程中的一个概念,函数在它的块中调用自己一次或多次。然而,由于函数不断地调用自己,这些类型的函数经常会遇到速度问题。记忆通过存储已经计算过的值以备后用来帮助这个过程。我们先来多了解一下递归函数。
为了跟上这一课,让我们从之前的笔记本文件“ Week_08 ”继续,并在底部简单地添加一个标记单元格,上面写着“递归函数和记忆化”。
理解递归函数
所有的递归函数都有一个所谓的基本情况,或者一个停止点。像循环一样,您需要一种方法来中断递归调用。如果没有一个,你会创建一个最终会崩溃的无限循环。例如,假设我们为以下问题设定了一个基础案例 1 :
-
你能计算出 5 的总数吗?
-
你能算出 5∫4 的和吗?
-
你能计算出 5÷4÷3 的和吗?
-
你能算出 5∫4∫3∫2 的和吗?
-
你能算出 5∫4∫3∫2∫1 的和吗?
-
是的,我们达到了我们的基本情况;结果是 120。
在这个例子中,我们从 5 开始递归调用,希望在计算总数之前达到我们的基本情况。在每次新的调用中,我们在表达式中增加一个数字,这个数字是前一个数字减一。这是一个阶乘函数执行递归调用的例子。根据任务的不同,函数可以同时执行两次递归调用。最明显的例子就是斐波那契数列。我们一起编程。
你可能会问自己,这些有什么用?通常,您可以编写一个循环来执行与递归调用相同的任务。那么为什么要使用它们呢?在某些情况下,递归函数比编写循环更容易理解。因为会出现重复的任务,所以经常在搜索和排序算法中使用它们。假设您需要搜索一个四维数组,也就是所谓的列表中的列表中的列表。您可以编写一个递归函数,在每次发现新的维度时调用自己,而不是编写一堆 for 循环来遍历每个列表。代码将产生更少的行,并且更容易阅读。让我们来看看一些例子!
编写阶乘函数
阶乘是递归的一个比较简单的例子,因为它们是给定数字乘以所有先前数字直到达到零的结果。让我们试着给它编程:
# writing a factorial using recursive functions
def factorial(n):
# set your base case!
if n <= 1:
return 1
else:
return factorial( n – 1 ) * n
print( factorial(5) ) # the result of 5 * 4 * 3 * 2 * 1
去查查手机。从前面的例子中我们知道,我们将得到一个输出 120 。递归调用发生在 else 块中。return 语句在自身内部调用 factorial 函数,因为为了得到 factorial(5),的结果,它必须计算“factorial(4)÷5”。然后它必须计算"阶乘(3)÷4"才能得到阶乘(4) 的结果,如下所示:
-
阶乘(5) =阶乘(4)÷5
-
阶乘(4) =阶乘(3)÷4
-
阶乘(3) =阶乘(2)÷3
-
阶乘(2) =阶乘(1)÷2
-
阶乘(1) = 1
这种情况一直发生,直到在 factorial(1) 达到基本情况,该情况没有递归调用,并返回值 1 。一旦达到基本情况,它就可以开始将所有计算值返回到原始调用,如下所示:
-
阶乘(1) = 1
-
系数(2)= 1 ' 2 = 2
-
系数(3)= 3 ' 3 = 6
-
系数(4)= 9 ' 4 = 24
-
系数(5)= 24’5 = 120
递归函数向下工作,直到达到基本情况。一旦返回一个值,它就可以返回到以前的调用并返回一个结果。
斐波那契数列
斐波那契数列是数学中最著名的公式之一。它也是编程中最著名的递归函数之一。序列中的每个数字都是前面两个数字的和,因此 fib(5) = fib(4) + fib(3) 。斐波纳契数列的基本情况是 0 和 1 ,因为 fib(2) 的结果是“ fib(2) = fib(1) + fib(0) ”。为了创建递归序列,我们需要返回两个值以下的值:
# writing the recursive fibonacci sequence
def fib(n):
if n <= 1:
return n
else:
return fib( n – 1 ) + fib( n – 2 )
print( fib(5) ) # results in 5
去查查手机。我们得到 5 作为输出。记住这不是 3 + 4 的结果,而是 fib(3) + fib(4) 的结果。斐波那契数列在一次返回中使用了两次递归调用,这使得它比我们的阶乘函数复杂得多。为了计算 fib(5) , fib(1) 必须计算五次。这是因为两部分递归调用。当这些递归调用发生时,它们基本上分解成一个金字塔状的结构。例如,让我们看看图 8-1 。
图 8-1
斐波那契数列递归序列树
该图表示为了计算 fib(5) 的结果而需要发生的所有递归调用。随着传入的数量增加,递归调用的结构和数量也在增加。它是指数级的,可以显著降低程序的速度。即使试图执行 fib(40) 也需要几分钟,而 fib(100) 通常会因为最大递归深度问题而中断。这就引出了我们的下一个话题,如何解决这个问题… 记忆。
理解记忆
当你第一次访问一个网页时,你的浏览器需要一点时间来加载该网页所需的图像和文件。当你第二次进入完全相同的页面时,它通常会加载得更快。这是因为你的浏览器使用了一种叫做“缓存的技术当您第一次加载页面时,它会将图像和文件保存在本地。第二次访问该网页时,它没有重新下载所有的图像和文件,而是直接从缓存中加载。这改善了我们的网络体验。
在计算中,记忆化是一种优化技术,主要用于通过存储先前调用函数的结果并在尝试计算相同序列时返回保存的结果来加速计算机程序。这就是所谓的“??”缓存,“??”,前面的段落是内存化如何提高性能的真实例子。让我们看一些记忆化如何改进递归函数的例子。
使用记忆
为了将记忆应用到斐波纳契数列,我们必须了解缓存值的最佳方法是什么。在 Python 中,字典让我们能够基于给定的键存储值。它们也是基于常数时间,用大 O 符号表示。我们将在下周讨论这个话题。现在,只要明白字典在返回信息方面比大多数其他数据集合要快得多。由于字典的速度和独特的键结构,我们可以用它们来存储每个斐波那契数列的值。这样,一旦已经计算了像 fib(3) 这样的单个序列,就不需要再次计算。它被简单地存储在缓存中,并在需要时被检索。让我们试一试:
1| # using memoization with the fibonacci sequence
3| cache = { } # used to cache values to be used later
5| def fib(n):
6| if n in cache:
7| return cache[ n ] # return value stored in dictionary
9| result = 0
11| # base case
12| if n < = 1:
13| result = n
14| else:
15| result = fib( n – 1 ) + fib( n -2 )
17| cache[ n ] = result # save result into dictionary with n as the key
19| return result
21| print( fib(50) ) # calculates almost instantly
去查查手机。请注意,这次它几乎可以立即计算出 fib(50) 。如果我们在不缓存值的情况下运行,执行同样的计算可能需要几个小时或几天。这就是记忆在工作中的美妙之处。这个过程从将参数传递到 fib 开始。然后程序检查参数是否作为关键字出现在缓存中。如果有,它只是返回值。然而,如果不是,它需要通过使用递归来计算正确的结果,直到达到基本情况。一旦达到基数,值就开始作为键值对保存在缓存中。当递归调用开始沿着结构向上返回时,它们只是从字典中取出值。由于记忆化,它只需计算一次,而不必计算上千次。
注意
记忆不是完美的;单个缓存中可以存储的数据量是有限制的。
使用@lru_cache
既然我们已经知道如何自己创建一个缓存系统,那么让我们使用 Python 的内置方法来实现内存化。它被称为" lru_cache 或最近最少使用的缓存。它执行的方式和我们之前的记忆技术是一样的;然而,它会用更少的代码行来完成,因为我们把它作为一个装饰器来应用。让我们来看看:
# using @lru_cache, Python’s default moization/caching technique
from functools import lru_cache
@lru_cache( ) # python’s built-in memoization/caching system
def fib(n):
if n <= 1:
return n
else:
return fib( n – 1 ) + fib( n – 2 )
fib(50) # calculates almost instantly
去查查手机。我们将获得与前一个单元格相同的输出,但这次使用的行数更少。它执行完全相同的技术,除了它作为装饰器而不是直接在函数中应用。就性能而言,没有更好的方法,但是使用 lru_cache 看起来更舒服。
周四练习
-
Factorial Caching :将 lru_cache 内置装饰器应用于我们之前创建的 Factorial 函数,或者建立自己的缓存系统。
-
搜索数据:创建一个接受两个参数的函数,一个数据列表和一个要搜索的项目。搜索传入的数据列表,如果要搜索的项目出现,则返回 True,否则,返回 False。如果其中一项是另一个列表,创建一个递归调用,这样就不需要创建另一个循环。使用下面的示例调用作为预期数据的参考:
>>> searchList( [ 2, 3, [ 18, 22 ], 6 ], 22 )
今天,我们学习了所有的递归函数,以及如何用记忆的概念来改进它们。我们能够使用简单的缓存技术来存储以前计算的值。递归函数在有意义的时候会很有用,但是在大多数情况下,一个简单的 for 循环就足够了,因为递归函数会随着时间变得很慢。
星期五:写二分搜索法
本周的项目是关于理解编程中最有效的算法之一… 二分搜索法。当你需要搜索一个充满数据的列表时,你需要高效地完成它。为十个条目的列表创建一个算法可能没有意义,但是想象一下如果它是一百万个条目。你不希望在列表中一项一项地搜索,试图找到你要找的东西。相反,我们使用像二分搜索法这样的算法来执行这些任务。
为了跟上这一课,让我们从之前的笔记本文件“ Week_08 ”继续,并在底部添加一个减价单元格,上面写着:“星期五项目:写一个二分搜索法。
完工图纸
尽管程序本身相对较小,但我们必须理解二分搜索法算法是如何工作的。对于本周的设计概念,我们将列出需要遵循的步骤。记住算法只不过是一组步骤。二分搜索法也不例外。该算法的每个步骤如下:
-
对列表进行排序。
-
找到中间的索引。
-
检查中间索引处的值;如果它是我们要寻找的值,则返回 True。
-
检查中间索引处的值;如果它比我们要找的值大,就切掉列表的右半部分。
-
检查中间索引处的值;如果它小于我们所要寻找的值,就切掉列表的左半部分。
-
重复步骤 2 到 6,直到列表为空。
-
如果 while 循环结束,意味着没有剩余的项目,所以返回 False。
让我们通过一个例子和下面的参数: [ 14,0,6,32,8 ],,我们将寻找数字 14 。参见表 8-1 进行逐步走查。
表 8-1
二分搜索法示例描述
|步骤
|
变量的值
|
描述
|
密码
| | --- | --- | --- | --- | | one | 列表:[0,6,8,14,32] | 立即对列表进行排序 | list.sort( ) | | Two | 中期:2 | 找到中间,5 / 2,向下舍入 | len(list) // 2 | | three | 值:8 | 不要还真,8 不是 14 | 列表【2】 | | four | 条件:假 | 8 小于 14 不跑挡 | 如果列表【2】>14 | | five | 列表:[14,32] | 运行程序块,删除列表的前半部分 | list = list[mid + 1 : ] | | Two | mid: 1 | 中间索引是 1,因为 2 / 2 | len(list) // 2 | | three | 数值:32 | 不要还真,32 不是 14 | 列表【1】 | | four | 列表:[14] | 运行块,删除列表的后半部分 | list = list[ : mid - 1] | | Two | mid: 0 | 找到中间,1 / 2,向下舍入 | len(list) // 2 | | three | 返回 True | 中间索引的值为 14,返回 True | 返回真值 |
线性搜索需要我们逐项搜索列表,看看我们要找的号码是否在列表中。当考虑效率和搜索需要多长时间来完成任务时,它将基于列表的长度。随着列表长度的增长,找到我们要找的数字所需的时间也在增长。然而,使用二分搜索法,即使列表中有一百万个数字,在列表中查找一个数字所需的时间也只需要很少的步骤。例如,当你搜索一个有一百万个数字的列表时,线性搜索可能要尝试一百万次才能找到这个数字,但是二分搜索法可以在 20 次猜测内找到它。当它搜索时,它将列表切成两半。在 10 次猜测之内,你已经在处理一个不到 2000 个条目的列表了。这就是高效算法的妙处。让我们一起走过每一步,了解算法是如何编程的。
程序设置
在我们开始编写算法之前,我们需要建立一种方法来生成一个随机的数字列表。让我们导入 random 模块并使用 list comprehension 生成一些数据:
1| # setting up imports and generating a list of random numbers to work with
2| import random
4| nums = [ random.randint(0, 20) for i in range(10) ] # create a list of ten numbers between 0 and 20
6| print( sorted(nums) ) # for debugging purposes
去查查手机。我们导入了 random 模块,以便用我们的列表理解生成一个 20 个随机数的列表。出于调试目的,我们在第 6 行输出了一个经过排序的版本 nums ,以便查看我们将要处理的数据。
第一步:给列表排序
算法的第一步是对列表进行排序。一般来说,在传入列表之前要对其进行排序,但是我们希望采取所有的预防措施,确保该算法甚至可以处理未排序的列表。让我们首先定义函数定义,并对传入的列表进行排序:
4| nums = [ random.randint(0, 20) for i in range(10) ] # create a ... ◽◽◽
6| def binarySearch(aList, num):
7| # step 1: sort the list
8| aList.sort( )
10| print( sorted(nums) ) # for debugging purposes
12| print( binarySearch(nums, 3) )
我们已经在底部添加了函数调用,并将打印返回值,但是现在当您运行单元格时,什么也不会发生。让我们进入第二步。
第二步:找到中间的索引
在这一步,我们需要找到中间的索引。我说的不是列表中间项目的值,而是实际的索引号。如果我们正在搜索一个包含一百万个条目的列表,中间的索引将是 500,000。该索引处的值可以是任何数字,但同样,这不是本步骤的目的。让我们写出第二步:
8| aList.sort( ) ◽◽◽
10| # step 2: find the middle index
11| mid = len(aList) // 2 # two slashes means floor division – round down to the nearest whole num
13| print(mid) # remove once working
15| print( sorted(nums) ) # for debugging purposes ◽◽◽
去查查手机。为了找到中间的索引,我们需要将列表的长度除以 2,然后向下舍入到最接近的整数。我们需要使用整数,因为一个索引只能是一个整数。你永远无法访问索引 1.5 。此外,我们向下舍入,因为向上舍入会导致指数超出范围的错误。例如,如果列表中有一个项目,那么 1 / 2 = 0.5 并且向上舍入到 1 将导致错误,因为列表中的单个项目在索引 0 处。输出结果将是 5 ,因为我们正在处理一个包含十个数字的列表。完成后,继续删除第 13 行的 print 语句。
步骤 3:检查中间索引处的值
现在我们有了中间的索引,我们想看看给定索引处的值是否是我们要寻找的数字。如果是,那么我们要返回真值:
11| mid = len(aList) // 2 # two slashes ... ◽◽◽
13| # step 3: check the value at middle index, if it is equal to num return True
14| if aList[mid] == num:
15| return True
17| print( sorted(nums) ) # for debugging purposes ◽◽◽
去查查手机。根据为你随机生成的列表,你将得到一个输出,或者是真或者是无。如果数字 3 出现在索引 5 处,那么您的输出将为真,因为我们在第 14 行的条件为真,并将运行返回语句。
步骤 4:检查值是否更大
如果我们要找的数字不在中间的索引处,那么我们需要找出要删除列表的哪一半。让我们首先检查中间索引处的值是否大于我们要搜索的数字。如果是,我们需要删除列表的右半部分:
15| return True ◽◽◽
17| # step 4: check if value is greater, if so, cut off right half of list using slicing
18| elif aList[mid] > num:
19| aList = aList[ : mid ]
21| print(aList) # remove after working properly
23| print( sorted(nums) ) # for debugging purposes ◽◽◽
去查查手机。在第 18 行,我们检查列表中间索引处的值是否大于我们在函数调用过程中传递的参数。不过,第 19 行是二分搜索法的神奇之处。使用切片,我们能够将列表的值重新声明到列表的前半部分。
注意
请记住,切片允许您输入开始、停止和步进。如果你没有像前面那样输入一个数字,这意味着你在使用默认值。默认值为 start = 0、stop = len(list)和 step = 1。
我们的意思是,我们希望保留从索引 0 到中间索引的所有项目。完成后删除第 21 行,因为它将简单地输出我们新的列表的结果。
步骤 5:检查值是否小于
这一步与第 4 步完全相同,但条件相反。如果中间索引的值小于我们要寻找的数字,我们要删除左半部分:
19| aList = aList[ : mid ] ◽◽◽
21| # step 5: check if value is less, if so, cut off left half of list using slicing
21| elif aList[mid] < num:
22| aList = aList[ mid + 1 : ]
23| print(aList) # remove after working properly
25| print( sorted(nums) ) # for debugging purposes ◽◽◽
去查查手机。在第 22 行,我们执行与步骤 4 相反的切片。这次我们声明" mid + 1 "因为我们不想包含中间的索引,因为它已经被检查过了。这个逻辑现在已经在我们的二分搜索法上实现了。剩下的就是建立一个循环来重复步骤 2 到 5,如果我们没有找到我们想要的东西,就返回 False 。
步骤 6:建立一个循环来重复步骤
我们需要循环,直到找到参数,或者直到列表为空。这听起来像是一个 while 循环的好例子。在创建了 while 语句之后,我们需要确保在循环中执行步骤 2 到 5 的代码:
8| aList.sort( ) ◽◽◽
10| # step 6: setup a loop to repeat steps 2 through 6 until list is empty
11| while aList:
12| mid = len(aList) // 2
14| if aList[mid] == num:
15| return True
16| elif aList[mid] > num:
17| aList = aList[ : mid ]
18| elif aList[mid] < num:
19| aList = aList[ mid + 1 : ]
21| print(aList) # remove after working properly
21| print( sorted(nums) ) # for debugging purposes ◽◽◽
去查查手机。我们的二分搜索法现在正在执行所有必要的步骤,要么在找到参数时返回 True ,要么创建一个空列表,在这种情况下,循环将结束。请记住,我们前面的 while 语句与“while len(list)>0:”相同。如果循环结束,剩下的就是返回 False ,因为这意味着列表中不包含我们的号码。
步骤 7:否则返回 False
为了完成我们的二分搜索法,我们只需在之后返回 False ,同时循环结束:
19| aList = aList[ mid + 1 : ] ◽◽◽
21| # step 7: return False, if it makes it to this line it means the list was empty and num wasn’t found
22| return False
24| print( sorted(nums) ) # for debugging purposes ◽◽◽
去查查手机。我们现在已经完成了二分搜索法算法!现在,当你运行这个单元时,你将得到一个输出,或者是真的 ?? 或者是假的 ?? 的 ??。您可以在 while 循环中随意打印列表,这样您就可以看到列表在每一步是如何被截断的。
最终输出
你可以在 Github 资源库中找到本周以及这个项目的所有代码。下面的最终输出将不包括我们在前面的块中添加的任何注释,因此您可以清楚地看到完整的版本:
1| # full output of binary search without comments
2| import random
4| nums = [ random.randint(0, 20) for i in range(10) ]
6| def binarySearch(aList, num):
7| aList.sort( )
9| while aList:
10| mid = len(aList) // 2
12| if aList[mid] == num:
13| return True
14| elif aList[mid] > num:
15| aList = aList[ : mid ]
16| elif aList[mid] < num:
17| aList = aList[ mid + 1 : ]
19| return False
21| print( sorted(nums) )
22| print( binarySearch(nums, 3) )
去查查手机。如果遇到任何问题,请务必参考这段代码。尝试增加传入列表中的项目数,看看它能多快找到您的编号。即使在大的列表中,这个算法也会以极快的速度执行。
今天不仅对理解二分搜索法如何工作很重要,而且对理解我们如何从一系列逐步指令中编写算法也很重要。算法可能很容易理解,但很难翻译成代码。使用这种算法,我们可以开始理解搜索是如何高效的,即使有大量数据需要筛选。
每周总结
在这一周中,我们讨论了 Python 中一些更高级的主题。当您开始构建您的编程体验时,您应该始终考虑效率。首先,我们需要确保我们的程序在执行时是正确的,但是我们也需要知道它们的速度。如果一个算法或程序能给你一只股票精确的价格,但要花十年时间去执行,那它就一文不值了。这就是一个伟大算法的重要性。除了效率,我们还想记住代码的可读性。尽管 sing list comprehension、lambdas 和递归函数不能提高我们程序的速度,但它有助于提高我们阅读正在发生的事情的能力。在下周的课程中,我们将讨论算法的复杂性和使用某些数据类型时性能的重要性。
挑战问题解决方案
接下来,你可以找到本周挑战问题的答案:
1| # ask user for input, return whether it is prime or not
3| def isPrime(num):
4| for i in range( 2, int(num**0.5) + 1 ):
5| if num % i == 0:
6| return False
7| else:
8| return True
10| n = int( input("Type a number: ") )
12| if isPrime(n):
13| print("That is a prime number.")
14| else:
15| print("That is not a prime number")
这个程序最重要的部分在第 4 行。虽然你可能已经得到了正确的,我们想创建这个程序,以便它是有效的。第 4 行的语句也可能如下所示:
>>> for i in range(2, num):
然而,这条生产线的问题是效率不高。当你试图计算一个数是否是质数时,这个数的平方根是你需要的最大值。如果一个数不能被 2 和它自身的平方根整除,那么这意味着它是一个质数。如果我们不计算传入数字的平方根来计算质数,那么我们将不得不一直循环到质数本身。让我们以数字 97 为例,它是一个质数。使用第二个 for 循环语句,我们将总共循环 96 次迭代。然而,将语句写在代码块中,我们将只循环总共九次迭代。随着传入的数字变大,迭代次数也变多。因此,在编程时牢记效率总是很重要的。
每周挑战
要测试你的技能,请尝试以下挑战:
-
递归二分搜索法:把我们一起创建的二分搜索法算法变成一个递归函数。与其使用 while 循环,不如调用自己来减少列表并最终返回真或假。
-
高效算法:看看我们写的二分搜索法,你怎么可能让它更高效呢?
-
区分大小写的搜索:重写二分搜索法,使其能够处理包含数字和字母的列表。它应该区分大小写。使用下面的函数调用来理解传入的参数。提示:【22】<‘a’将返回 True 。
>>> binarySearch( [ 'a', 22, '3', 'hello', 1022, 4, 'e' ] , 'hello') # returns True
九、高级主题 II:复杂性
本周是高级 python 概念的继续,将涵盖开发人员在工作中必须了解的更多主题。
本周开始,我们将介绍一个你一直在使用的概念,生成器和迭代器。在接下来的几天里,我们将讨论装饰者和模块,它们将帮助我们构建更大规模的应用程序。这些概念将有助于理解如何使用框架,比如 Flask 和 Django 。
虽然我不喜欢在这本书里谈论理论,但理解时间复杂性如何与算法一起工作是很重要的。周四,我们将深入大 O 符号并进一步理解算法。本书中的所有课程都引导你能够继续深造,成为一名 Python 开发人员。这一切将我们带入周五的项目,即面试准备。因为这本书是作为改善或改变你职业生涯的工具而设立的,所以其中很重要的一部分就是面试过程。会有关于这个过程的信息,会有什么期待,以及如何处理一些你可能会被问到的面试问题。
概述
-
理解生成器和迭代器对象
-
使用和应用装饰器
-
创建和导入模块
-
什么是时间复杂度和大 O 记数法?
-
知道如何处理面试、问题等等
挑战问题
作为一名程序员,你必须考虑执行一个程序所花费的时间。即使是一个会给你 100%准确答案的程序,如果没有及时把答案给你,也可能毫无用处。不用查,你认为当需要检索和存储信息时,列表或字典更有效吗?
星期一:生成器和迭代器
在本书的前几节,你可能已经看到了提到的单词生成器或迭代器。在不知情的情况下,你一直在使用它们。今天,我们将深入了解这些概念是什么以及如何使用它们。
为了继续今天的内容,让我们从“ python_bootcamp ”文件夹中打开 Jupyter 笔记本。打开后,创建一个新文件,并将其重命名为“ Week_09。“接下来,制作第一个标题为:“生成器和迭代器”的单元格。“我们将开始在那个牢房下面工作。
迭代器与可迭代对象
一个迭代器是一个包含可以被迭代的条目的对象,这意味着你可以遍历所有的值。一个可迭代是一个集合,比如列表、字典、元组和集合。主要的区别是可迭代的不是迭代器;相反,它们是数据的容器。在 Python 中,迭代器对象实现了神奇的方法 iter 和 next ,允许你遍历它的值。
创建基本迭代器
我们可以很容易地从可迭代器中创建迭代器。您可以简单地使用 iter() 函数来实现:
1| # creating a basic iterator from an iterable
3| sports = [ "baseball", "soccer", "football", "hockey", "basketball" ]
5| my_iter = iter(sports)
7| print( next(my_iter) ) # outputs first item
8| print( next(my_iter) ) # outputs second item
10| for item in my_iter:
11| print(item)
13| print( next(my_iter) ) # will produce error
去查查手机。迭代器将永远记住它们返回的最后一项,这就是为什么我们在第 13 行得到一个错误。使用 next() 方法,我们能够输出迭代器中的下一项。然而,一旦迭代器中的所有项都被使用了,我们就不能再遍历迭代器了,因为没有剩余的项了。迭代器也非常适合循环,像列表和字典一样,我们可以简单地使用 in 关键字*(见第 10 行)*。你仍然可以像我们通常做的那样循环遍历列表,它总是从索引 0 开始,但是一旦我们的迭代器没有条目了,我们就不能再使用它了。
创建我们自己的迭代器
现在我们已经看到了如何从 Python iterable 创建迭代器,让我们创建自己的迭代器类,它将输出字母表中的每个字母。要创建一个迭代器,我们需要实现神奇的方法 iter() 和 next():
1| # creating our own iterator
3| class Alphabet( ):
4| def __iter__(self):
5| self.letters = "abcdefghijklmnopqrstuvwxyz"
6| self.index = 0
7| return self
9| def __next__(self):
10| if self.index <= 25:
11| char = self.letters[ self.index ]
12| self.index += 1
13| return char
14| else:
15| raise StopIteration
17| for char in Alphabet( ):
18| print(char)
去查查手机。输出结果是一次打印一个字母的整个字母表。我们首先创建一个名为" Alphabet 的迭代器。然后我们使用 iter 方法来声明与这个迭代器相关的属性。将 iter 方法视为迭代器的初始化方法。在 iter 方法结束时,必须始终返回 self 。声明了 next 方法,这样当被调用时,迭代器可以返回字母串中的下一个字符。我们存储了一个名为 index 的属性,以便跟踪接下来应该返回哪个条目。最后,我们在第 14 行添加了一个条件,这样如果已经输出了所有的字母,它就会产生一个 StopIteration 错误。当您需要以特定的方式遍历 Python 集合时,迭代器非常有用。
什么是发电机?
生成器是产生反馈信息以产生一系列结果而不是单个值的函数。它们是简化迭代器创建的一种方式。通常,当函数完成任务并返回信息时,函数内部声明的变量将被删除。然而,对于生成器,它们使用“ yield ”关键字将信息发送回被调用的位置,而不会终止函数。生成器并不总是必须返回整数,尽管您可以生成您想要的任何信息。让我们看几个既有数字又有单个字符的例子。
注意
生成器是简化的迭代器。
创建范围生成器
虽然 range 函数不是一个生成器,但是我们可以使用 yield 关键字从生成器创建我们自己的版本。让我们试一试:
1| # creating our own range generator with start, stop, and step parameters
3| def myRange(stop, start=0, step=1):
4| while start < stop:
5| print( "Generator Start Value: { }".format(start) )
6| yield start
7| start += step # increment start, otherwise infinite loop
9| for x in myRange(5):
10| print( "For Loop X Value: { }".format(x) )
去查查手机。这两个 print 语句用于显示发生器 myRange 何时被访问,与循环的何时输出结果相比较。我们能够调用 myRange 就像我们调用一个普通的 Range 函数一样,因为发电机的工作方式。在第 3 行,我们像声明其他函数一样声明该函数,接受与 range 相同的参数。我们在第 4 行的函数中开始一个 while 循环,这个循环将使返回到 start 值。一旦信息被返回给 for 循环,它就能够将该值用于当前的迭代。一旦 for 循环完成了它的代码块,它就返回到生成器,因为 while 循环条件没有得到满足。通常,一旦函数返回了信息,就不会再被调用;但是,生成器会继续返回并存储信息,直到它们的条件得到满足。如果我们没有用步骤增加开始值,我们将创建一个无限循环。与迭代器一样,当您需要特定的序列进行迭代时,生成器会很有用。当您需要知道内存时,生成器非常有用。尽管它们在性能方面没有那么高效,但在存储信息时它们是内存高效的。它们在需要创建数据管道的情况下很有用,这是指需要对数据片段执行一组执行。**
周一练习
-
反向迭代:创建一个接受列表的迭代器,当迭代结束时,它以相反的顺序返回信息。提示:在迭代器中接受参数时,需要使用 init 方法,以及 iter 和 next 。下面的调用应该产生**“5,4,3,2,1”**。
>>> for i in RevIter( [ 1, 2, 3, 4, 5 ] ): -
Squares :创建一个类似 range 函数的生成器,除了它每次都产生一个平方数。下面这个调用的结果应该是**“0,1,4,16”**。
>>> for i in range(4):
今天,我们能够理解如何构建我们自己的 range 函数,以及如何迭代数据集合。生成器是迭代器的简化版本,但是使用 yield 关键字返回信息。迭代器必须总是通过使用 iter 和 next 方法来创建,并且对于创建我们自己的迭代序列非常有用。
星期二:装修工
如果你想学习框架,或者了解如何改进 Python 中的功能,那么你需要理解什么是装饰器以及它是如何工作的。这将有助于简化我们的代码,同时减少改进程序所需的代码行。
为了跟上这一课,让我们从之前的笔记本文件“ Week_09 ”继续,简单地在底部添加一个标有“ Decorators”的 markdown 单元格。
什么是装修工?
装饰器,也被称为包装器,是在不显式修改其他函数的情况下赋予它们额外能力的函数。它们由函数名前面的**“@”**符号表示,该符号写在函数声明的上方,如下所示:
>>> @decorator
>>> def normalFunc( ):
当您希望在函数执行之前或之后执行某些功能时,Decorators 非常有用。例如,让我们假设您想要基于用户登录来限制对某个功能的访问。您可以将代码放入装饰器中,并将装饰器应用于所有函数,而不是为您创建的每个函数编写相同的条件语句。现在,无论何时调用一个函数,条件语句仍然会运行,但是您可以节省几行代码。这是 Flask 框架的一个真实例子,它使用 decorators 基于用户认证限制对某些页面的访问。今天晚些时候我们会看到一个最小的例子。
高阶函数
一个高阶函数是一个对其他函数进行操作的函数,要么把一个函数作为它的自变量,要么返回一个函数。在上周的课程中,我们看到了用 lambdas,map,filter 和 reduce 完成的。装饰器是高阶函数,因为它们接受一个函数并返回一个函数。
创建和应用装饰器
为了创建装饰器,我们需要声明一个接受另一个函数作为参数的函数。在这个装饰器中,我们可以定义另一个要返回的函数,它将运行作为参数传入的函数。我们来看看这是怎么写的:
1| # creating and applying our own decorator using the @ symbol
3| def decorator(func):
4| def wrap( ):
5| print("======")
6| func( )
7| print("======")
8| return wrap
10| @decorator
11| def printName( ):
12| print("John!")
14| printName( )
去查查手机。我们将得到一个" John!"以名字上下的等号作为边框。在第 10 行,我们将装饰器附加到了 printName 函数上。每当调用 printName 函数时,装饰器就会运行,并且 printName 会作为“ func 的参数传入。在装饰器中,我们声明了一个名为包装的函数。这个 wrap 函数会打印一个边框,然后调用 func 参数,再打印另一个边框。记住装饰者必须返回一个函数才能运行。我们声明的装饰器可以附加到我们编写的任何函数上。使用这个装饰器的所有函数将简单地运行,它们的上下都有一个边框。
带参数的装饰器
虽然 decorators 只是简单地给函数增加了额外的功能,但是它们也可以像其他函数一样有参数。让我们以下面的例子为例,我们想要运行函数 x 次:
1| # creating a decorator that takes in parameters
3| def run_times(num):
4| def wrap(func):
5| for i in range(num):
6| func( )
7| return wrap
9| @run_times(4)
10| def sayHello( ):
11| print("Hello!")
去查查手机。这个单元格会输出" Hello!“四次。当装饰器接受一个参数时,语法会改变。我们的装饰器这次接受了一个参数 num ,而 wrap 函数这次接受了该函数作为参数。在我们的 wrap 函数中,我们创建了一个 for 循环,它将运行附加到装饰器的函数,运行次数与第 9 行装饰器上声明的参数一样多。
注意
当向装饰器传递参数时,函数会自动运行,所以在这个实例中我们不需要调用 sayHello。
带有装饰符和参数的函数
当您需要一个函数接受参数,同时还附加了一个装饰器时, wrap 函数必须接受与原始函数完全相同的参数。让我们来试试:
1| # creating a decorator for a function that accepts parameters
3| def birthday(func):
4| def wrap(name, age):
5| func(name, age + 1)
6| return wrap
8| @birthday
9| def celebrate(name, age):
10| print( "Happy birthday { }, you are now { }.".format(name, age) )
12| celebrate("Paul", 43)
去查查手机。这将输出一个格式良好的字符串,其中包含第 12 行传入的信息。当我们调用庆祝时,装饰器接受庆祝作为 func 的参数,两个参数“保罗和“ 43 ”被传递到包装。当我们在包装中调用我们的函数时,我们将相同的参数传递给函数调用;然而,我们将年龄参数增加 1。
限制功能访问
你可能想知道装饰者如何服务于一个目的,因为最后几个单元格似乎没有意义。对于它们中的每一个,我们可以简单地在原始函数中添加这些行。不过这只是为了语法理解。Decorators 在框架中经常使用,它可以帮助您在框架中编写的许多函数增加功能。一个例子是能够基于用户登录凭证来限制对页面或功能的访问。让我们创建一个装饰器,如果密码不匹配,它将有助于限制访问:
1| # real world sim, restricting function access
3| def login_required(func):
4| def wrap(user):
5| password = input("What is the password?")
6| if password == user["password"]:
7| func(user)
8| else:
9| print("Access Denied")
10| return wrap
12| @login_required
13| def restrictedFunc(user):
14| print( "Access granted, welcome { }".format(user[ "name" ]) )
16| user = { "name" : "Jess", "password" : "ilywpf" }
18| restrictedFunc(user)
去查查手机。在第 13 行,我们声明了一个普通的函数,它接受一个用户,并输出一个包含他们的名字和可访问性的语句。我们的装饰器附在第 12 行,这样当我们调用 restrictedFunc 并传入我们创建的用户时,它将通过装饰器运行。在 wrap 函数中,我们要求用户输入密码,并在第 6 行检查密码是否正确。如果他们输入正确的密码,那么我们允许他们访问该功能并打印出“访问授权”。然而,如果密码不正确,那么我们输出“拒绝访问”,并且从不运行 restrictedFunc 。这是一个简单的例子,说明了 Flask 如何处理页面的用户限制,但是它证明了装饰者的重要性。我们现在可以将 login_required 附加到我们认为应该只由用户访问的任何功能上。
星期二练习
-
用户输入:创建一个装饰器,要求用户输入一个数字,只有当数字小于 100 时,才运行这个函数。该功能应该简单地输出“小于 100 ”。在下面使用函数声明:
>>> @decorator >>> def numbers( ): >>> print("Less than 100") -
创建路由:创建一个装饰器,它接受一个字符串作为参数,一个包装函数接受 func 。让 wrap 函数打印出字符串,并运行传入的函数。传入的函数不需要做任何事情。在 Flask 中,您可以通过使用接受 URL 字符串的 decorators 来创建页面。使用下面的函数声明来启动:
>>> @route("/index") >>> def index( ): >>> print("This is how web pages are made in Flask")
今天是为使用 Python 的其他技术(比如框架)做准备的重要一课。装饰器有助于改善函数的执行,可以附加到任何必要的函数上。这有助于减少代码并提供改进的功能。
星期三:模块
大多数程序倾向于包含许多行代码,以至于你不能把它们都存储在一个文件中。相反,您将代码分成几个文件,这有助于保持项目的组织性。这些文件中的每一个都称为模块。这些模块中有变量、函数、类等。,您可以将其导入到项目中。幸运的是,Python 拥有一大批开发人员,他们创建模块供我们使用,以增强我们自己的项目。今天,我们将看看 Python 中包含的一些模块,如何导入它们,如何使用它们,以及如何编写我们自己的模块在 Jupyter Notebook 中使用。
为了跟上这一课,让我们从笔记本文件“ Week_09 ”继续,只需在底部添加一个标有“模块”的降价单元格。
导入模块
在接下来的几个例子中,我们将使用 math 模块,这是 Python 的内置模块之一。这个特定的模块有函数和变量来帮助我们解决任何与数学相关的问题,无论是舍入、计算圆周率还是许多其他与数学相关的任务。对于第一个单元格,我们将导入整个数学模块及其内容:
# import the entire math module
import math
print( math.floor(2.5) ) # rounds down
print( math.ceil(2.5) ) # rounds up
print(math.pi)
去查查手机。我们会得到“ 2 ”、“ 3 ”和“ 3.14 的输出。当我们导入 math 时,我们能够访问 math 的所有函数、变量和类。在这个例子中,我们调用了存储在 math 模块中的两个函数和一个变量。为了导入整个模块及其内容,只需在模块名称前加上关键字 import。无论何时你想访问它的任何内容,你都需要使用点语法。现在我们可以使用任何数学的代码。
仅导入变量和函数
当您知道您不需要使用整个模块,而是需要使用几个函数或变量时,您可以直接导入它们。您应该始终确保只导入您需要的东西。在前一个单元格中,我们导入了整个 math 模块;然而,我们并不真的需要,因为我们只使用了其中的两个函数和一个变量。要导入特定的内容,您需要包括来自关键字的和您想要导入的内容的名称:
# importing only variables and functions rather than an entire module, better efficiency
from math import floor, pi
print( floor(2.5) )
# print( ceil(2.5) ) will cause error because we only imported floor and pi, not ceil and not all of math
print(pi)
去查查手机。我们将得到“ 2 ”和“ 3.14 ”的输出。导入模块的特定部分时,import 语句会稍有变化。要从单个模块中分离多个导入,可以使用逗号。我们注释掉了 ceil 的打印语句,因为它不起作用。我们只直接导入了 floor 和 pi ,没有导入 ceil 函数。注意,我们也不需要在名称前引用带有点语法的 math 模块。这是因为我们直接导入了 floor 函数和 pi 变量,所以现在可以不用点语法引用它们了。记住只导入你需要的东西。
注意
您可以像前面一样从模块中导入类;简单地使用类名。
使用化名
通常,您想要导入的内容的名称可能很长。您可以在导入时给出一个“别名或昵称,而不必每次都写出完整的名称:
# using the 'as' keyword to create an alias for imports
from math import floor as f
print( f(2.5) )
去查查手机。我们将得到与前两个单元格相同的输出,除了这次我们能够将 floor 函数引用为字母“ f ”。这是因为我们使用“作为关键字来编写我们的进口声明。您可以给导入的任何内容重新命名,尽管通常最好只给较大的名称重新命名。
创建我们自己的模块
现在我们知道了如何导入和调用一个模块,让我们创建自己的模块。继续打开你电脑上的任何文本编辑器,如记事本或文本编辑。在文件中编写以下代码,保存在您的“ Week_09 ”文件所在的同一个文件夹中,文件名为“ test.py ”。如果这两个文件不在同一个目录中,它会产生一个错误:
# creating our own module in a text editor
# variables to import later
length = 5
width = 10
# functions to import later
def printInfo(name, age):
print( "{ } is { } years old.".format(name, age) )
参见图 9-1 中文本编辑器中的代码示例。
图 9-1
文本编辑器(notepad++)中带有代码的 test.py 模块
您刚刚编写了第一个模块!请记住,模块只不过是在其他文件中编写的代码,我们可以将它们导入到我们的任何项目中。现在让我们看看如何使用它们。
使用我们在 jupiter 笔记本中的模块
在任何其他情况下,您都可以用来自关键字的导入和来导入我们在 test.py 中编写的变量和函数。然而,Jupyter Notebook 在使用您创建的模块时会有所不同。我们将使用“ run 命令来加载我们创建的整个模块。运行文件后,我们可以使用我们在模块中编写的变量和函数。让我们来看看如何做到这一点:
# using the run command with Jupyter Notebook to access our own modules
%run test.py
print(length, width)
printInfo("John Smith", 37) # able to call from the module because we ran the file in Jupyter above
去查查手机。您会注意到我们能够输出在 test.py 模块中声明的变量和函数打印语句。请记住,run 命令将文件当作单个单元格来运行。模块中的任何函数调用或打印语句都会立即运行。要测试这一点,请尝试在模块底部放置一个打印语句。当您在开发环境中工作时( IDE ,您将像平常一样编写导入,如下所示:
>>> from test import length, width, printInfo
这就是 Jupyter Notebook 处理我们创建的文件的方式。
注意
您可以将在 Python 文件夹中创建的任何模块放在硬盘上。一旦文件存在,就可以正常访问它们,而不用使用 run 命令。
周三练习
-
时间模块:导入时间模块,调用睡眠函数。让电池休眠 5 秒,然后打印“时间模块导入”。虽然我们还没有介绍这个模块,但是这个练习将为你提供一个很好的练习,让你自己尝试使用一个模块。请随意使用谷歌、Quora 等。
-
计算区域:创建一个名为“ calculation.py 的模块,该模块内部有一个单一的函数。该函数应该接受两个参数,并返回它们的乘积。我们可以想象我们正在试图计算一个矩形的面积,它需要接受长度和宽度属性。在 Jupyter Notebook 中运行该模块,并在单元格中使用以下函数调用:
>>> calcArea(15, 30)
今天的重点是关于模块,如何导入它们,如何使用它们,如何创建我们自己的模块,以及如何在 Jupyter Notebook 中调用我们自己的模块。理解模块是如何工作的会让你有能力使用 Python 中的框架。例如,Flask 使用了许多不同的模块,因为每个模块都有特定的用途。当你需要组织你的项目时,模块就是答案。
周四:理解算法复杂性
在这本书里,我们一直在边做边学。在开始时,我说过我们不会深入理论,而是通过一起构建项目和编码来学习。今天的重点主要是编程和算法的理论。如果编程中有一个理论你应该懂,那应该是大 O 记法。
为了跟上这一课,让我们从之前的笔记本文件“ Week_09 ”继续,并在底部简单地添加一个标记单元格,写着“理解算法复杂性”。
什么是大 O 记数法?
作为一名软件工程师,你经常需要估计一个程序的执行时间。为了给出一个合适的估计,你必须知道程序的时间复杂度。这就是算法复杂性发挥作用的地方,也称为大 O 符号。它是描述一个算法或程序执行需要多长时间的概念。以一个列表为例。随着列表中项目数量的增加,遍历列表所需的时间也在增加。这就是所谓的 O(n) ,其中 n 代表运算次数。之所以叫大 O 记法,是因为你在运算次数前面加了一个“大 O ”。
大 O 建立了一个最坏情况的运行时。即使你搜索了 100 个条目的列表,并且第一次就找到了你想要的,这仍然会被认为是 O(100) ,因为它可能需要多达 100 次操作。
最有效率的大 O 记数法是 O(1) ,也称为常数时间。这意味着无论需要多少项目或步骤,都将花费相同的时间,并且通常是即时发生的。如果我们采用相同的 100 个条目的列表并直接访问一个索引,这将被称为 O(1) 。我们将立即检索该索引中的值,而不需要遍历列表。
效率最低的时间复杂度之一是O(n∫2)。这是一个双循环的表示。我们编写的冒泡排序算法使用了双 for 循环,被认为是编程中效率较低的排序算法之一;然而,它很容易理解,因此是对算法的一个很好的介绍。我们将在今天晚些时候看到冒泡排序与另一种设计得更高效的算法相比是如何的。
当你将遍历列表中每个元素的简单搜索与像二分搜索法这样的高效算法进行比较时,你会发现它们不会随着时间以相同的速度增长。以表格 9-1 为例,该表格显示了搜索给定物品所需的时间。
表 9-1
大 O 批注增长率对比 1
|元素数量
|
简单搜索
|
二进位检索
| | --- | --- | --- | | 运行时用大 O 表示法 | O(n) | O(对数 n) | | Ten | 10 毫秒 | 3 毫秒 | | One hundred | 100 毫秒 | 7 毫秒 | | Ten thousand | 10 秒 | 14 毫秒 | | One billion | 11 天 | 32 毫秒 |
我们可以清楚地看到,有效的算法可以帮助提高我们的程序速度。因此,在编写代码时,记住效率和时间复杂性是很重要的。图 9-2 中的图片描绘了操作数量相对于元素数量的复杂性。
图 9-2
大 O 符号复杂度随时间变化图
这里没有涵盖所有的大 O 符号,所以如果你想进一步理解这些概念,一定要做进一步的研究。这只是对什么是大 O 以及为什么它在编写我们的程序时很重要的一个介绍。
散列表
当我们最初讨论字典时,我们非常简要地回顾了一下哈希。既然我们已经介绍了大 O 符号,理解散列表以及它们为什么重要就容易多了。字典可以以 O(1) 的复杂度被访问,因为它们是如何存储在内存中的。它们使用哈希表来存储键值对。在讨论哈希表之前,让我们快速回顾一下哈希函数以及如何使用它:
>>> a, c = 'bo', "bob"
>>> b = a
>>> print(hash(a), hash(b), hash(c))
从前面的代码中,我们将为 a 和 b 获得相同的值,并为 c 的散列获得单独的值。哈希函数用于创建给定值的整数表示。在这种情况下,字符串“ bo 的整数和变量 a 和 b 是相同的;但是,“ bob 和 c 变量完全不同,因为它们的值不同。
当字典将键值对存储到内存中时,它们使用了这个概念。哈希表用于存储哈希、键和值。当您需要通过键检索给定值时,会用到存储的散列。以表 9-2 为例。有三个键值对,都有不同的哈希值。当您想要访问名称的值时,您应该写:
>>> person[ "name" ]
所发生的是 Python 散列字符串" name "并寻找散列值而不是密钥本身。您可以把这想象成通过索引来检索列表中的项目。这是非常有效的,因为您可以在 O(1) 时间几乎立即检索基于散列的值。
表 9-2
Python 哈希表的逻辑表示
|混杂
|
钥匙
|
价值
| | --- | --- | --- | | Two billion eight hundred and thirty-nine million seven hundred and two thousand five hundred and seventy-two | 名字 | 约翰·史密斯 | | Eight billion two hundred and sixty-seven million three hundred and forty-eight thousand seven hundred and twelve | 年龄 | Thirty-two | | -2398350273 | 语言 | 计算机编程语言 |
字典是有用的数据集合,不仅可以保持信息的联系,还可以提高效率。当你试图回答编程问题或让程序运行得更快时,请记住这一点。像关于大 O 符号的信息一样,这只是对散列表的介绍。如果你想了解更多,一定要使用谷歌、Quora 等搜索。
字典与列表
为了理解哈希表和 Python 字典的真正威力,让我们将其与列表进行比较。我们将编写一个条件语句,让 Python 检查字典和列表中的给定项,并计算每一项需要多长时间。我们要把代码分成两个单元。第一个单元格将生成包含 1000 万个条目的字典和列表:
# creating data collections to test for time complexity
import time
d = { } # generate fake dictionary
for i in range(10000000):
d[ i ] = "value"
big_list = [ x for x in range(10000000) ] # generate fake list
去查查手机。什么都不会发生。我们只是在这个单元格中创建了变量,所以我们不必重新创建它们,因为根据您的计算机,这需要几秒钟的时间。在下面的单元格中,我们将为每次数据收集寻找最后一个元素所花费的时间设置一个计时器。我们将使用时间模块来跟踪开始和结束时间:
1| # retrieving information and tracking time to see which is faster
3| start_time = time.time( ) # tracking time for dictionary
5| if 9999999 in d:
6| print("Found in dictionary")
8| end_time = time.time( ) – start_time
10| print( "Elapsed time for dictionary: { }".format(end_time) )
12| start_time = time.time( ) # tracking time for list
14| if 9999999 in big_list:
15| print("Found in list")
17| end_time = time.time( ) – start_time
19| print( "Elapsed time for list: { }".format(end_time) )
去查查手机。在第 3 行和第 12 行,我们以 UTC 格式访问当前时间。检查完我们的条件后,我们再次获得 UTC 格式的当前时间;然而,我们从中减去开始时间,得到整个执行所用的秒数。你会注意到这两个时间有很大的不同。这个列表通常需要花费 1 到 1.5 秒,而字典几乎每次都是即时的。现在这看起来没有太大的区别,但是如果你需要搜索 1000 个项目呢?使用列表现在成了一个问题,因为字典会继续立即做,但是列表会花更长的时间。
注意
除非另有说明,时间模块以 UTC ( 通用时间)获取时间。UTC 始于 1970 年 1 月 1 日。输出 time.time()时看到的数字是从那天上午 12:00 开始的秒数。
算法之战
测试时间复杂度的一个最明显的方法是运行两个算法。这将让我们真正看到高效算法背后的力量。我们将测试冒泡排序和另一种叫做插入排序的排序算法。虽然插入排序不是排序时最有效的算法,但我们会发现它仍然比冒泡排序强大得多。让我们继续写下第一个单元格中的两个排序算法:
1| # testing bubble sort vs. insertion sort
3| def bubbleSort(aList):
4| for i in range( len(aList) ):
5| switched = False
6| for j in range( len(aList) – 1 ):
7| if aList[ j ] > aList[ j + 1 ]:
8| aList[ j ], aList[ j + 1 ] = aList[ j + 1 ], aList[ j ]
9| switched = True
10| if switched == False:
11| break
12| return aList
14| def insertionSort(aList):
15| for i in range( 1, len(aList) ):
16| if aList[ i ] < aList[ i – 1 ]:
17| for j in range( i, 0, -1 ):
18| if aList[ j ] < aList[ j – 1 ]:
19| aList[ j ], aList[ j + 1 ] = aList[ j + 1 ], aList[ j ]
20| else:
21| break
22| return aList
去查查手机。现在我们已经定义了需要调用的两个函数,让我们设置一些要排序的随机数据,并像在上一节中一样设置一个计时器:
1| # calling bubble sort and insertino sort
to test time complexity
2| from random import randint
4| nums = [ randint(0, 100) for x in range(5000) ]
6| start_time = time.time( ) # tracking time bubble sort
7| bubbleSort(nums)
8| end_time = time.time( ) – start_time
9| print( "Elapsed time for Bubble Sort: { }".format(end_time) )
11| start_time = time.time( ) # tracking time insertion sort
12| insertionSort(nums)
13| end_time = time.time( ) – start_time
14| print( "Elapsed time for Insertion Sort: { }".format(end_time) )
去查查手机。这甚至不是一场比赛。插入排序是一种比它的对应物更有效的算法。尽管两者都使用了双 for 循环的概念,但冒泡排序的步骤效率要低得多,因为它每次都是从列表的前面开始。在设计你的程序和算法时,记住时间复杂性总是很重要的。如果你不确定什么是最好的,试着像我们这里一样测试一下。
周四练习
-
归并排序:做一些研究,尝试找出归并排序算法的“大 O”表示。
-
二分搜索法:二分搜索法要在一千万个数字中找到一个数字,最多可以猜多少次?
虽然今天更多的是关于理论,而不是这本书的其他部分,但它是编程最重要的方面之一。大 O 符号帮助我们理解程序和算法的效率。理解我们为什么使用像字典或列表这样的数据集合总是很重要的。当效率很重要时,可以实现字典来改进程序。这是我们使用字典进行缓存的另一个原因。
周五:面试准备
如果你正在寻找一份新的职业或工作,比如 Python 开发人员,那么如果你不能通过面试,那么所有这些课程都是徒劳的。本周五,我们将讲述一个通用软件开发面试的过程。我们将涵盖每个阶段,面试前后要做什么,白板,回答一般性和技术性问题,以及如何勾画你的简历和个人资料。这一课对于那些在面试过程中挣扎的人或者那些从未参加过正式软件开发面试的人来说是有帮助的。如果你对这一部分不感兴趣,并希望继续下去,就把今天作为本书时间表的一个休息。
开发人员面试流程
开发人员角色的面试过程可以分为许多不同的阶段。在下文中,你会发现业内许多公司实践的主要阶段。请记住,这是一个一般的面试过程,并不是每个公司都会严格遵循这些流程。请将本节更多地用作可能发生的情况的指南:
- 第一阶段
- 关于你自己和过去工作经验的基本问题。第一步通常是打电话给第三方招聘人员、内部招聘人员、人力资源或公司的人才招聘人员。在面试过程的第一步,面试官试图判断你是否适合这个职位。他们希望你提到“流行语”,同时提供为什么你非常适合这个职位的信息。你想把自己和这个职位联系起来。一定要谈论你使用他们寻找的语言和技术的经历。面试官在找你满足一半的要求,让自己很匹配。没有人会知道所有的事情,但是向他们展示你所知道的和你学习的意愿是很好的。
注意
流行语是该职位寻找的关键词。例如,使用 Python 的后端职位会期望听到类似于 API、JSON、Python、Flask、Django、Jinja、关系数据库、PostgreSQL 等词。
-
第二阶段
- 如果你通过了电话面试,你通常会被要求参加一次面对面的面试。这个阶段通常是你遇到目前在公司工作的其他开发人员的地方。虽然他们会问你一些面试问题,但这个阶段通常是让员工看看他们是否愿意和你一起工作,并在更私人的层面上了解你。一般来说,你会一次面试一小部分员工。您将有两到五次这样的会议,每次持续 10 到 15 分钟。在雇用一个人之前,这些小组通常会聚在一起讨论下一阶段的潜在候选人。在这个阶段,一定要适当地介绍自己,和每个人握手。了解每一位员工,试着在个人层面上与他们建立联系。
-
第三阶段
- 这是技术回合。在这一阶段,将提出问题来评估开发人员的技能和能力。通常,会有一个白板问题、几个纸上技术问题和一个脑筋急转弯。这一阶段通常由招聘经理或你将共事的团队经理来进行。当被问到一个问题时,确保你清楚地理解它。非常欢迎您在回答问题之前,根据需要提出尽可能多的问题,以便清楚地了解问题。如果你不知道问题的答案,让面试官知道你没有用过这个概念或者没有看到问题。在这个阶段,面试官会知道你是否不知道自己在说什么,所以不要试图编造一些东西。他们会对你的诚实留下更深刻的印象,并试图引导你解决问题。在这个阶段,他们不在乎你是对是错。他们更感兴趣的是你如何思考,你解决问题的能力如何。
-
第四阶段
- 此时,你通常和招聘经理或人力资源人员坐在一起。在这个阶段,你可以问关于公司的问题,以及工作角色。如果你已经做到了这一步,公司已经看到了你作为一名潜在员工的价值。通常,这是合同谈判和薪资谈判的地方。在面试结束时,准备好要问的问题,而且要问很多。如果你没有问题,这通常是没有准备好或懒惰的表现。
面试前要做什么
几乎在你生活中做的每一件事上,你都要做好充分的准备。面试也是一样。以下是你在面试前应该做的一些提示:
-
研究
- 一定要研究你面试的公司。不要仅仅了解他们创造了什么产品,或者提供了什么服务,还要知道他们支持什么慈善机构,他们合作的公司等等。这表明你参与并关心公司的利益。一点点就够了。
-
做好准备
- 准备一个文件夹或文件夹,里面包括你的简历、一叠面试笔记纸、工作实例等。
-
简历
-
总是用高质量的纸打印简历。
-
让你的简历符合你要面试的工作。比如后端角色,提到 Python,SQL,数据库相关技术等。
-
保持你的简历在一页以内。
-
不要添油加醋。
-
用经验、技能和教育等部分来组织。
-
把你的简历想象成 30 秒的电梯推销。
-
通常,让设计师忽略你的简历会有所帮助。一些网站会收取很少的费用,但是会让你的简历看起来更专业,更有条理。
-
-
组合网 S ite
- 不是所有的开发者都有个人网站,但是如果你没有的话,看起来肯定很糟糕。想象一下去看一个没有牙齿的牙医。把你自己看作是你试图卖给公司的产品,你应该有一个网站来展示你的技能,并让别人联系你。
-
Github
-
几乎每个招聘机构和公司都会通过你的 Github 来查看你参与过的项目。
-
最好在你的投资组合中也有完整的项目。一个大项目总会比 10 个小项目更突出。
-
在你的简历、作品集网站和电子邮件中包含你的 Github 账户。
-
-
LinkedIn
-
大多数招聘人员和公司登录 LinkedIn 只有一个原因,那就是寻找潜在的求职者。
-
确保你的个人资料是最新的,包含所有相关信息和你参与过的项目。
-
你的个人资料图片应该是专业的。你不需要穿西装打领带,但是最好不要有你在沙滩上的照片。
-
把这个网站看作是你的专业网络服务。
-
经常发布你想从事的领域的信息。你发布的内容越多,招聘人员就越容易认出你。
-
-
社交媒体
- 保密或保持干净。你最好相信公司会看你的帖子,以了解你是谁,如果他们不喜欢他们看到的,你就不会得到回复。
-
直接申请
- 直接向公司提交申请看起来总是更专业。通常,你会在 Indeed 或 ZipRecruiter 上找到一份自己喜欢的工作;然而,这些公司每天都被这些网站上的申请淹没,他们通常有算法来淘汰大多数候选人。发送一封直接的电子邮件表明你投入了时间和精力去直接联系公司。
一般问题
下面是一个非技术性问题的列表,后面是一个好答案的例子。选择这些问题是因为它们通常被不恰当地提问和回答:
-
你期望的薪水是多少?
-
“我现在没有确切的数字。我想对其他公司提供的类似职位做一些调查。这个职位你给员工的平均工资是多少?”
-
当他们询问时,千万不要说出一个数字,这在任何谈判过程中都会给他们提供筹码。
-
用另一个问题来反驳他们的问题。
-
如果他们继续问你一个数字,简单地陈述同样的回答。
-
-
你认为自己五年后会怎样?
-
“未来五年,我会更加关注自己的技能。我知道,专注于继续我的教育和自我提升将引领我到达我需要的地方。”
-
专注于提高你的技能表现出同情心。
-
-
你为什么想成为一名软件开发人员?
-
“我一直对能够从无到有创造出一些东西很感兴趣,我也一直喜欢挑战。当你能够解决问题并构建应用程序时,那是一种美妙的感觉。”
-
展示你作为开发人员的激情;这将永远是一种优势。
-
永远不要提是钱的问题,即使是。
-
-
你为什么要转行?
-
“我觉得我在以前的职业生涯中没有受到足够的挑战,我一直对编程感兴趣,并对构建改善人们生活的应用程序感到兴奋。”
-
像上一个问题一样,展示你对这个职业的热情和动力。
-
解释你喜欢接受挑战,这表明你并不懒惰。
-
永远不要提是钱的问题,即使是。
-
-
你为什么想在这里工作?
-
“你们在这里构建的应用程序帮助了世界各地如此多的用户,我很乐意成为其中的一员。”
-
谈论公司合作的应用程序或慈善机构。这表明你有激情,在团队中工作出色,而且你有动力。
-
提及公司文化也是一个很好的答案。
-
不提工资,福利,甚至更惨…没有答案。
-
-
告诉我一个棘手的软件问题,以及你是如何解决的。
-
“我当时正在做一个项目,我被指派在应用程序中实现 Steam API。不幸的是,API 无法正确连接。使用调试器,我在导入和函数调用位置设置了断点。在意识到他们根本没有被击中后,我想这一定是连接的问题。在尝试了几种导入方式并通读了文档之后,我决定将应用程序设置为在函数被点击时关闭。当我第二次运行这个程序时,它立刻就关闭了。意识到函数正在被调用,但是应用程序没有正常运行,我认为这一定是一个导入问题。直到我在一个更新的应用程序中测试该 API 时,才发现问题是由于代码是在 2.2 版本中编写的,而该 API 需要 3.6 版本。为了连接 API,我必须通过 mapper 函数手动导入库,该函数可以在版本之间翻译代码。在意识到映射器工作后,我能够实现 Steam API 包含在其 SDK 中的库。”
-
尽可能深入地研究这个问题。他们想知道导致问题的每一个细节,你是如何解决问题的,以及你试图解决问题的所有想法。虽然前面的答案可能对你来说没有太大的意义,但它显示了问题,我做了什么来试图找到问题,以及一旦我找到问题,我是如何想出解决方案的。
-
白板和技术问题
这一部分列出了在白板和技术问题的面试流程的第三阶段您应该考虑使用的技巧:
-
慢慢来
- 绝对不要急着解决问题。在回答问题之前,先考虑一个合适的解决方案。通常,给定时间后,你会想到两三种不同的解决方案。
-
大声说出
-
总是谈论你的思考过程。这会让面试官感觉更舒服,这样你们就不会坐在一个安静的房间里思考了。
-
这向面试官展示了你解决问题的能力。
-
即使你没有给出正确的答案,他们至少能明白你错在哪里,并提供一些指导。
-
-
步骤>语法
-
写白板时,你需要当着面试官的面在白板上写下一个功能或几行代码。要记住的最重要的事情是,你的思维过程比你的实际代码更重要。
-
你可以在白板上有语法错误,但仍然可以通过面试;然而,不正确的算法或步骤会导致失败。
-
-
提问
-
如果你不确定,问问题。当试图解决一个问题时,提出问题是完全可以的。
-
记住你问的问题很重要。问一个排序方法是做什么的,和你想让我用什么类型的排序方法相比,有很大的不同。
-
-
算法复杂度
-
永远记住算法的复杂性。在你写完代码后,你通常会被问到是否有办法进一步提高代码的性能。
-
知道你刚写的算法的大 O 符号范畴。
-
考虑什么样的数据类型或集合最适合您的场景。
-
-
要诚实
-
如果你不知道一个答案,绝对不要试图通过你的方式说话。这个阶段的面试官是一个专业的开发人员,可以挑出任何没有意义的东西。
-
诚实地说你不确定,但愿意学习这些材料,这将被证明是回答你不知道如何解决的问题的更好方法。
-
面试问题结束
你绝不会希望在面试结束时,当他们问你是否有任何问题时,你两手空空。在面试中做笔记,写下你想到的问题通常是一个好习惯。在下文中,你会发现一系列你应该考虑问的问题:
-
通勤情况如何?
-
停车是免费的吗?
-
你会举办社交活动吗?
-
如果我想进一步发展我的职业技能,你们会提供服务或学费补偿吗?
-
你们提供什么样的福利?
-
公司文化是什么样的?
-
有多少人将和我一起在团队中工作?
-
会有辅导吗?
-
你能告诉我更多关于这个职位的日常职责吗?
-
在这家公司工作,你最喜欢的是什么?
-
对于这个职位的人来说,在这个公司里典型的职业道路是什么?
-
面试过程的下一步是什么?
-
在典型的一天里,我会期待些什么?
-
这家公司支持哪些慈善机构?
-
有什么公司活动吗,比如运动队?
面试后做什么
即使你通过了前三个阶段,如果你没有在面试后执行正确的步骤,你仍然会悲惨地失败。在下文中,你会发现一些例子,告诉你在面试过程结束后应该做些什么:
-
跟进
- 总是,总是,总是立即给面试官发一封电子邮件,感谢他们抽出时间。这表示尊重,也是一种礼貌的姿态。
-
自我批判
- 理解自己的错误。不要把它当成个人问题;你能变得更好的唯一方法是理解和自我反省。
-
继续建造
-
总是致力于项目,努力改善你的投资组合。
-
与最新的库、语言和技术保持同步。
-
经常更新你的简历和作品集。
-
-
冒险外出
-
出去参加当地的社交活动。在这里你会遇到很多熟人。当你认识在公司工作的人时,总是更容易找到工作。
-
像 code alongs 或 hackathons 这样的活动是结识其他希望一起工作的开发人员的好方法。
-
-
拒绝
- 这种情况时有发生,你不会总是得到这份工作。如果真的发生了,一定要礼貌地问面试官为什么你没有得到这份工作。不要往心里去;相反,利用这些信息成为更好的开发人员并进行改进。
今天的主题是了解面试过程以及如何提高面试技巧。即使是最伟大的程序员也可能是糟糕的面试官。找到一份合适的工作需要大量的努力和专注,即使这样,也可能不会成功。最好的建议是继续提高你的技能,并与其他软件开发人员建立联系。
每周总结
本周是更高级的 Python 概念的第二部分。本周教授的许多课程不仅对面试很重要,而且对提高你的项目表现也很重要。迭代器和生成器是一种可以用来创建更好的循环结构和算法的对象。能够使用 decorators 将有助于提高函数能力,并且在 Flask 或 Django 这样的框架中被广泛使用。模块允许我们通过将函数或整个文件导入我们的程序来使用其他开发者的代码。能够编写我们自己的模块允许我们减少每个文件中的代码量。您通常希望尽可能保持组织有序,因为这使得项目更容易阅读、维护和修复。然而,如果这周你需要理解一个主题,那就是大 O 符号。了解大 O 是如何工作的,可以在求职面试中有所帮助,知道如何提高申请的速度。还有更多关于 Python 和编程的高级主题,但这最后两周将为您提供足够的时间来开始构建自己的项目,甚至继续学习框架和使用数据库的大型应用程序。
挑战问题解决方案
在周四的课程中,我们复习了这个问题的确切答案。显而易见,字典显然是存储和检索数据的更有效的方式。在处理大型数据集时,记住要使用的正确数据结构总是很重要的。可以肯定的是,在面试过程中也会问到类似的问题。
每周挑战
要测试你的技能,请尝试以下挑战:
-
了解市场:上 Indeed 或 Monster 这样的求职网站,寻找你感兴趣的潜在工作。记下他们寻找的资格和技术。看了几个职位描述,排名前三的技术是什么?这些应该是你前进的重点。
-
购物车模块:从我们几周前编写的购物车程序中取出代码,并将其放入一个模块中。在 Jupyter 笔记本上,运行模块,让程序正常工作。
-
增强的购物车:在程序中增加一个新功能,允许用户保存购物车。在运行程序时,应该加载保存的购物车。该方法应该在模块中编写。提示:使用 CSV 或文本文件。
-
代码大战:在
www.codewars.com上算账,想办法解决一些问题。代码战争已被用于面试练习问题,提高你的算法和解决问题的技能,等等。这将有助于提高本书教授的技能。试着每天解决一个问题,你会发现你的 Python 编程技能会提高。
https://guide.freecodecamp.org/computer-science/notation/big-o-notation/