本文先介绍了Python中程序、模块和包的基本使用,并在此基础上介绍了Python标准库。然后详细介绍了Python中的文件IO操作,包括文本文件、二进制文件的读写和其他IO操作。最后介绍了面向对象,包括类的定义、继承的使用、鸭子类型和魔法方法。
之前我们使用的.ipynb
文件都不是纯Python文件,纯Python文件应该是.py
文件。
一个纯Python文件,如report.py可能如下:
def get_description():
"""Return random weather, just like the pros"""
from random import choice
possibilities = ['rain', 'snow', 'sleet', 'fog', 'sun', 'who knows']
return choice(possibilities)
def get_desc():
"""Return random weather, just like the pros"""
from random import choice
possibilities = ['rain', 'snow', 'sleet', 'fog', 'sun', 'who knows']
return choice(possibilities)
这个代码中包含了两个函数。
如需本节同步
ipynb
文件和Python文件,可以直接点击加QQ群 963624318 在群文件夹商业数据分析从入门到入职中下载即可。
.ipynb
文件也可以另存为.py
文件,依次选择文件 → 下载 → Python(.py)
即可,就会保存为与.ipynb
文件同名的.py
文件。
可以在Python IDE如PyCharm中运行Python文件,也可以在命令行中运行,假如你的Python文件在E:\Test
目录下,名为test.py,则可以先在命令行中通过命令cd E:\Test
切换到Python文件所在目录,然后再执行python test.py
即可运行当前文件。
可以在其他程序中直接使用自己定义的Python代码。
例如可以在当前的.ipynb
中使用之前的report.py中的get_description()
函数(需要保证.ipynb
文件与report.py位于同级目录),如下:
import report
report.get_description()
输出:
'rain'
可以看到,正常执行。
之前我们可能已经遇到过类似如下的情况:
import math
math.sqrt(2)
通过import
关键字导入math
,然后就可以使用它进行各种操作了。
但是并不是可以任意导入的,如下:
import corley
此时报错:
---------------------------------------------------------------------------
ModuleNotFoundError Traceback (most recent call last)
<ipython-input-2-fb30571dbbc8> in <module>
----> 1 import corley
ModuleNotFoundError: No module named 'corley'
即提示没有名为“corley”的模块。
但是math
不在当前目录,也能正常使用,这是因为当前Python的模块搜索路径还包括其他路径,可以通过以下代码查看当前的模块搜索路径:
import sys
sys.path
输出:
['XXX',
'E:\\Anaconda3\\python38.zip',
'E:\\Anaconda3\\DLLs',
'E:\\Anaconda3\\lib',
'E:\\Anaconda3',
'',
'E:\\Anaconda3\\lib\\site-packages',
'E:\\Anaconda3\\lib\\site-packages\\win32',
'E:\\Anaconda3\\lib\\site-packages\\win32\\lib',
'E:\\Anaconda3\\lib\\site-packages\\Pythonwin',
'E:\\Anaconda3\\lib\\site-packages\\IPython\\extensions',
'C:\\Users\\LENOVO\\.ipython']
除了第一个目录是档期那目录,其他目录都是Anaconda的相关路径,math
模块可能就在某个路径下。
某些模块可能有长名称,在这种情况下,可以在导入时重命名模块,这样可以节省一些时间。
如下:
import report as rp
rp.get_description()
输出:
'sun'
可以看到,能达到同样的效果。
有些模块中可能有大量的定义,如果不需要全部导入,则可以从中导入需要使用的内容。
如下:
from report import get_description
get_description()
输出:
'fog'
也可以进行命名,如下:
from report import get_description as get_weather
get_weather()
输出:
'who knows'
从一行代码到多行函数,再到独立程序,再到同一目录中的多个模块,为了使Python应用程序更具规模,可以将模块组织成称为包的文件层次结构。
例如,当前目录下有一个子目录为source,下有两个文件,daily.py如下:
def forecast():
'fake daily forecast'
return 'like yesterday'
weekly.py如下:
def forecast():
"""Fake weekly forecast"""
return ['snow', 'more snow', 'sleet', 'freezing rain', 'rain', 'fog', 'hail']
此时可以从包source中的模块daily和weekly中导入forecast。
如下:
from source import daily, weekly
print('Daily forecast:', daily.forecast())
print('Weekly forecast:', weekly.forecast())
输出:
Daily forecast: like yesterday
Weekly forecast: ['snow', 'more snow', 'sleet', 'freezing rain', 'rain', 'fog', 'hail']
从Python3.3开始引入了隐式名称空间包,它允许我们创建一个不带__init__.py文件的包。
Python的一个突出的声明是它有“batteris included”——一个大型的标准模块库,它执行许多有用的任务,并被分开保存以避免核心语言膨胀。
不时地对Python的标准库进行一些探索总是很有帮助的。
Python的标准库非常广泛,提供了广泛的工具,如https://docs.python.org/3/library/列出的长目录所示。该库包含内置模块(用C编写),这些模块提供对系统功能(如Python程序员无法访问的文件I/O)的访问,以及用Python编写的模块,它们为日常编程中出现的许多问题提供标准化解决方案。其中一些模块被明确设计为鼓励和增强Python程序的可移植性,方法是将平台细节抽象到平台无关的api中。
用于Windows平台的Python安装程序通常包括整个标准库,并且通常还包括许多附加组件。对于类Unix的操作系统,Python通常是作为包的集合提供的,因此可能需要使用操作系统提供的打包工具来获取部分或全部可选组件。
例如datetime库的简单使用如下:
import datetime
dt = datetime.date(2020, 9, 28)
dt.weekday()
输出:
0
如果导入一个库报错ModuleNotFoundError,那说明这个库不是Python标准库,而是第三方库,需要下载安装之后才能导入。
安装第三方库可以使用conda
或pip
命令:
如安装jieba库(中文分词库)则可以执行conda install jieba
或pip install jieba
来安装这个库,也可以到pip官网https://pypi.org/project/pip/查找所需要的库并下载安装。
当程序运行时,所有生成的数据都存储在RAM中,RAM速度快,但有两个限制:
磁盘驱动器比RAM慢,但更便宜,并且更重要的是,即使断电也能保存数据,为了保持数据的持久性,我们需要将其作为文件存储在磁盘驱动程序中。
简单的持久性是一种最简单的平面文件,它只是存储在文件名下的字节序列,可以将文件读入内存并从内存写入文件(在磁盘驱动程序上)。
在读或写文件之前,必须先打开它:
fileobj = open(filename, mode)
mode是一个字符串,指示文件的类型以及要对其执行的操作:
mode的第一个字母表示操作:
字符 | 含义 |
---|---|
r | 读 |
w | 写入(如果文件不存在,创建一个;如果文件存在,则重写它) |
x | 写入(仅当文件不存在时) |
a | 如果文件存在,则追加(在结尾后写入) |
mode的第二个字母表示文件的类型:
字符 | 含义 |
---|---|
t(或无) | 纯文本 |
b | 二进制 |
查看open()
函数如下:
?open
输出:
Signature:
open(
file,
mode='r',
buffering=-1,
encoding=None,
errors=None,
newline=None,
closefd=True,
opener=None,
)
Docstring:
Open file and return a stream. Raise OSError upon failure.
file is either a text or byte string giving the name (and the path
if the file isn't in the current working directory) of the file to
be opened or an integer file descriptor of the file to be
wrapped. (If a file descriptor is given, it is closed when the
returned I/O object is closed, unless closefd is set to False.)
mode is an optional string that specifies the mode in which the file
is opened. It defaults to 'r' which means open for reading in text
mode. Other common values are 'w' for writing (truncating the file if
it already exists), 'x' for creating and writing to a new file, and
'a' for appending (which on some Unix systems, means that all writes
append to the end of the file regardless of the current seek position).
In text mode, if encoding is not specified the encoding used is platform
dependent: locale.getpreferredencoding(False) is called to get the
current locale encoding. (For reading and writing raw bytes use binary
mode and leave encoding unspecified.)
简单使用如下:
text = '''\
First line
Second line
Third line
End
'''
print(len(text))
fout = open('new_file.txt', 'wt')
ret = fout.write(text)
print('return value of write() =', ret)
fout.close()
输出:
38
return value of write() = 38
write()
方法的返回值为文件的长度。
同时可以看到,在同级目录下多了一个文件即为new_file.txt,内容如下:
First line
Second line
Third line
End
再读取文件如下:
fin = open('new_file.txt', 'r')
lines = fin.readlines()
print(lines)
fin.close()
输出:
['First line\n', 'Second line\n', 'Third line\n', 'End\n']
可以看到,将所有行读出并存到列表中。
再追加内容,如下:
text = '''\
1
2
3
'''
print(len(text))
fout = open('new_file.txt', 'a')
ret = fout.write(text)
print('return value of write() =', ret)
fout.close()
输出:
6
return value of write() = 6
再次查看new_file.txt,如下:
First line
Second line
Third line
End
1
2
3
可以看到,内容追加到之前的内容后面。
还可以通过print()
函数实现将文本输出到文件中,如下:
text = '''\
hello
world
'''
fout = open('file.txt', 'w')
print(text, file=fout)
fout.close()
此时可以看到目录中多了一个文件为file.txt,内容为:
hello
world
这就是通过print()
函数将内容输出到文件中。
Python有一个彩蛋,输入:
import this
输出:
The Zen of Python, by Tim Peters
Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea -- let's do more of those!
这就是Python之禅,定义了Python的一些规范。
可以分段写入文件,避免文件太大时导致的内存不足问题。
如下:
zen_of_py = '''\
The Zen of Python, by Tim Peters
Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea -- let's do more of those!
'''
fout = open('zen_of_py.txt', 'w')
size = len(zen_of_py)
offset = 0
chunk = 100
while offset < size:
fout.write(zen_of_py[offset : offset+chunk])
offset += chunk
fout.close()
此时查看目录,多了文件为zen_of_py.txt;
这是通过分段的方式写入文件的,类似于从网上下载文件分段下载。
读文件也能分段读取,也可以避免文件太大时内存不足。
如下:
zen_of_py = ''
fin = open('zen_of_py.txt', 'r')
chunk = 100
while True:
fragment = fin.read(chunk)
if not fragment:
break
zen_of_py += fragment
fin.close()
print(zen_of_py)
输出:
The Zen of Python, by Tim Peters
Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea -- let's do more of those!
也可以逐行读取文件。
如下:
fin = open('zen_of_py.txt', 'r')
while True:
line = fin.readline()
if not line:
break
print(line, end='')
fin.close()
输出与前面相同。
生成二进制数据如下:
bdata = bytes(range(256))
print(len(bdata))
print(bdata[0])
print(bdata[255])
写入二进制字节如下:
fout = open('bfile', 'wb')
ret = fout.write(bdata)
fout.close()
print('Write %d bytes' % ret)
输出:
Write 256 bytes
图片、音频、视频等文件都属于二进制文件。
也可以分段写入:
fout = open('bfile', 'wb')
size = len(bdata)
offset = 0
chunk = 100
while offset < size:
ret = fout.write(bdata[offset : offset+chunk])
print(ret)
offset += chunk
fout.close()
输出:
100
100
56
读取二进制文件如下:
fin = open('bfile', 'rb')
bdata = fin.read()
fin.close()
len(bdata)
输出:
256
如果打开了一个文件却忘了关闭它,当跳出打开文件的范围时,Python将关闭它。但是,更安全的方法是使用with
关键字。
如下:
with open('tmp1', 'w') as fout:
fout.write(text)
此时在当前目录下会生成文件tmp1。
读写时,Python会跟踪在文件中的当前位置,tell()
返回文件开头的当前偏移量(字节),seek()
跳转到文件中的另一个字节偏移量。
对文本文件的用法如下:
fin = open('tmp1', 'r')
print('starting pos =', fin.tell())
line = fin.readline()
print('# char read =', len(line))
print('ending pos =', fin.tell())
fin.close()
输出:
starting pos = 0
# char read = 6
ending pos = 7
这只适用于ASCII编码的文本文件,其中每个字符都存储在一个字节中。像UTF-8这样的编码可以使用每个字符不同数量的字节。
对二进制文件的操作如下:
fin = open('bfile', 'rb')
fin.seek(255)
bdata = fin.read()
print(len(bdata))
print(bdata[0])
输出:
1
255
seek()
方法也可以使用第二个参数,即seek(offset, origin)
:
如果origin为0(默认值),则从开始处偏移字节;
如果origin为1,则从当前位置偏移;
如果origin为2,则相对于终点偏移字节(即偏移量必须为负)。
origin不是关键字参数。
如下:
fin = open('bfile', 'rb')
fin.seek(-1, 2)
bdata = fin.read()
print(len(bdata))
print(bdata[0])
输出:
1
255
对于纯文本文件,唯一的组织级别是行(用换行符分隔),有时可能想要一个更丰富的结构,一种方法是引入额外的分隔符。
常见的形式如下:
\t
、,
、|
<
、>
{
、}
、[
、]
写入csv如下:
import csv
dc_heros = [
['Flash', 'Barry Allen'],
['Green Arrow', 'Oliver Queen'],
['Atom', 'Ray Palmer'],
['Bat Man', 'Bruce Wayne']
]
with open('dc_heros.csv', 'w') as fout:
csvout = csv.writer(fout)
csvout.writerows(dc_heros)
执行后,可以看到目录中生成了dc_heros.csv文件。
读csv文件如下:
with open('dc_heros.csv', 'r') as fin:
csvin = csv.reader(fin)
dc_heros = [row for row in csvin]
print(dc_heros)
输出:
[['Flash', 'Barry Allen'], [], ['Green Arrow', 'Oliver Queen'], [], ['Atom', 'Ray Palmer'], [], ['Bat Man', 'Bruce Wayne'], []]
Object Oriented Programming即面向对象程序设计。
Python中一切皆是对象,从数字到模块。
然而,Python通过特殊语法的mena隐藏了大部分对象机制,例如可以直接输入num=7
来创建一个值为7的integer类型的对象,并为名称num指定一个对象引用。
唯一需要查看对象内部的时间是想要创建自己的对象或修改现有对象的行为时。
对象包含:
它代表了一个特殊的例子:
把物体看作名词,把它们的方法看作动词。
如果一个对象像一个盒子,那么一个类就像制造盒子的模具。
定义一个空对象如下:
class Person():
pass
someone = Person()
type(someone)
输出:
__main__.Person
可以看到,定义了一个叫Person的类,并通过这个类实例化了一个实例,即someone对象。
生成一个整型对象,如下:
a = int(2)
type(a)
输出:
int
可以在生成对象即初始化时就给对象一些特征,此时可以给类定义__init__()
方法即初始化方法。
如下:
class Person():
def __init__(self, name, gender): # The first parameter has to be self
self.name = name
self.gender = gender
ed = Person('Edward', 'Male')
可以看到,__init__()
方法中传递了self
参数,这是在类中定义实例方法必须要带的参数,代表的是个体对象本身。
创建对象的大概过程如下:
(1)查找Person类的定义;
(2)在内存中创建新对象;
(3)调用__init__()
方法,将新创建的对象作为self传递,其他对象作为name和gender传递;
(4)在对象中存储name和gender的值;
(5)返回新对象;
(6)将创建的对象赋值给ed。
此时可以获取到创建出来的对象的属性,如下:
print('Name:', ed.name)
print('Gender:', ed.gender)
输出:
Name: Edward
Gender: Male
还可以修改对象的属性,如下:
ed.name = 'Corley'
ed.name
输出:
'Corley'
还可以在类中定义方法。
如下:
class Person():
def __init__(self, name, gender): # The first parameter has to be self
self.name = name
self.gender = gender
def say(self):
print("Hi I'm " + self.name + ", it's nice to meet you!")
ed = Person('Corley', 'Male')
ed.say()
输出:
Hi I'm Corley, it's nice to meet you!
还可以从现有类中创建一个新类,但需要添加或更改,这就是继承。
定义一个继承自Person类的类:
class MDPerson(Person):
pass
ed = MDPerson("Corley", 'Male')
ed.say()
输出:
Hi I'm Corley, it's nice to meet you!
可以看到,MDPerson类继承自Person类,虽然其内部没有实现其他代码,但是继承了父类中的全部特性,因此可以进行初始化和调用函数;
其中,MDPerson类称为子类,Person类称为父类。
子类还可以扩展新特性。
如下:
class MDPerson(Person):
def diagnose(self):
print('You need some treatment.')
ed = MDPerson("Corley", 'Male')
ed.diagnose()
输出:
You need some treatment.
但是父类中不能调用子类中新实现的特性。
如下:
someone = Person('Someone', 'NA')
someone.diagnose()
会报错:
----------------------------------------------------------------
AttributeError Traceback (most recent call last)
<ipython-input-11-e646e6a12c4e> in <module>
1 someone = Person('Someone', 'NA')
----> 2 someone.diagnose()
AttributeError: 'Person' object has no attribute 'diagnose'
子类中可以重新定义父类中已经定义过的方法,称之为重写。
如下:
class MDPerson(Person):
def __init__(self, name, gender, dept='Cardiac Surgery'):
self.name = 'Doctor ' + name
self.gender = gender
self.dept = dept
def say(self):
print("Hi I'm %s from %s department, how can I help you" % (self.name, self.dept))
ed = MDPerson("Corley", 'Male')
ed.say()
输出:
Hi I'm Doctor Corley from Cardiac Surgery department, how can I help you
显然,此时与父类中的方法不同。
可以通过super()
继承来自父类的特性,从而同时实现付类和子类的特性。
如下:
class MDPerson(Person):
def __init__(self, name, gender, dept='Cardiac Surgery'):
super().__init__(name, gender)
self.name = 'Doctor ' + self.name
self.dept = dept
def say(self):
super().say()
print("Hi I'm %s from %s department, how can I help you" % (self.name, self.dept))
ed = MDPerson("Corley", 'Male')
ed.say()
输出:
Hi I'm Doctor Corley, it's nice to meet you!
Hi I'm Doctor Corley from Cardiac Surgery department, how can I help you
如果Person的定义将来发生更改,那么使用super()
将确保MDPerson从Person继承的属性和方法将反映更改。
Python使用self
参数来查找正确对象的属性和方法。
如下:
ed = Person('Corley', 'Male')
ed.say()
Person.say(ed)
输出:
Hi I'm Corley, it's nice to meet you!
Hi I'm Corley, it's nice to meet you!
可以看到,两种调用方式的效果相同,这是因为类中实现的实例方法本来就有一个参数self
,代表对象自己,如果传一个参数,只要这个参数是对象,也是能够正常执行的。
背后执行的逻辑如下:
(1)查找对象ed的类(Person);
(2)将对象ed作为self参数传递给Person类的say()
方法。
再如:
ed = MDPerson('Corley', 'Male')
print(ed.name)
Person.say(ed)
输出:
Doctor Corley
Hi I'm Doctor Corley, it's nice to meet you!
可以看到,先使用MDPerson类进行初始化,初始化后ed对象的name属性为Doctor Corley
,其父类Person再调用say()
方法,并将ed作为对象传递进去,因此打印出的不是Hi I'm Corley, it's nice to meet you!
,也不是Hi I'm Doctor Corley from Cardiac Surgery department, how can I help you
,而是Hi I'm Doctor Corley, it's nice to meet you!
。
Python对多态性有一个松散的实现,这意味着它对不同的对象应用相同的操作,而不管它们是什么类。
同样,这也是EAFP设计模式的一部分。Python还会假设一个对象有这样的方法,并尝试调用它。如果没有找到方法,它将抛出异常。
如下:
class Quote():
def __init__(self, person, words):
self.person = person
self.words = words
def who(self):
return self.person
def says(self):
return self.words + '.'
class QuestionQuote(Quote):
def says(self):
return self.words + '?'
class ExclamationQuote(Quote):
def says(self):
return self.words + '!'
def who_says(obj):
print(obj.who(), 'says:', obj.says())
q1 = Quote('Edward', 'Normal quote')
q2 = QuestionQuote('Corley', 'Question quote')
q3 = ExclamationQuote('Jack', 'Exclamation quote')
quotes = [q1, q2, q3]
for q in quotes:
who_says(q)
输出:
Edward says: Normal quote.
Corley says: Question quote?
Jack says: Exclamation quote!
再定义一个与前面的3个类无关系的类,如下:
class Ed():
def who(self):
return 'Ed'
def says(self):
return "Hi I'm Edward :)"
ed = Ed()
who_says(ed)
输出:
Ed says: Hi I'm Edward :)
与前面的类混合使用:
quotes = [q1, q2, q3, ed]
for q in quotes:
who_says(q)
Ed.says(q3)
输出:
Edward says: Normal quote.
Corley says: Question quote?
Jack says: Exclamation quote!
Ed says: Hi I'm Edward :)
"Hi I'm Edward :)"
可以看到,即便Ed类和ExclamationQuote类毫无关系,但是ExclamationQuote对象还是可以作为参数传递到Education类的say()
方法中。
如果它像鸭子一样走路,像鸭子一样嘎嘎叫,那它就是鸭子。这也就是多态,使用起来比其它语言更加灵活,没有类继承等方面的严格限制。
当输入像a = 3 + 8
这样的代码时,可能想知道整数对象是如何知道如何实现+
运算的,并且是如何使用=得到结果的,这些操作符使用了Python的特殊方法(又名魔法方法)。
这些特殊方法的名称都以双下划线__
开始和结束,就像__init__()
方法一样。
例如,在为重写__str__()
方法时,打印对象如下:
print(q1)
输出:
<__main__.Quote object at 0x0000020B6FA17400>
显然,格式很不友好。
此时可以重写__str__()
方法,来自定义打印对象的形式,如下;
class Quote():
def __init__(self, person, words):
self.person = person
self.words = words
def __str__(self):
return 'Quote(%s, %s)' %(self.person, self.words)
def who(self):
return self.person
def says(self):
return self.words + '.'
q1 = Quote('Edward', 'Normal quote')
print(q1)
输出:
Quote(Edward, Normal quote)
显然,此时打印对象时,是打印的自定义字符串。
而如果不通过打印、而是直接像如下方式输出:
q1
会输出:
<__main__.Quote at 0x20b6f5c2970>
这可以通过__repr__()
重写方法,如下:
class Quote():
def __init__(self, person, words):
self.person = person
self.words = words
def __str__(self):
return 'Quote(%s, %s)' %(self.person, self.words)
def __repr__(self):
return 'Quote(%s, %s)' %(self.person, self.words)
def who(self):
return self.person
def says(self):
return self.words + '.'
q1 = Quote('Edward', 'Normal quote')
q1
输出:
Quote(Edward, Normal quote)
还有很多其他的魔法方法,可以根据需要选择使用。
Python之所以可以获得广泛的应用,一个很重要的原因就是广泛的库支持,不仅可以自定义模块和包,还可以使用Python标准库和第三方库,大大增加了Python的功能。Python中的文件IO操作也很方便,可以对文本文件、二进制文件、格式化文件进行读写,并进行其他文件操作。面向对象是Python的一大特性,但是相比于其他语言限制更少、更加灵活,具有继承、多态等特性,可以灵活使用。