likes
comments
collection
share

Python入门篇(中)

作者站长头像
站长
· 阅读数 17

目录

  1. 函数
  2. 迭代与生成器
  3. 面向对象
  4. 错误处理
  5. 总结

引言

上次我们学了 Python入门篇(上),讲了 Python 的一些简单特性和基本操作,忘记的同学可以回顾下。

今天我们继续学习,本文主要从函数、Python 特有的迭代/生成器,面向对象,错误处理来介绍 Python 的重点知识。

学了本篇内容,用 Python 来进行大多数业务开发就已经足够了,或作为入门基础,其它相关知识点也能触类旁通。

1. 函数

1.1 函数定义与调用

和别的语言一样,函数是一个完整的代码块,用于实现特定的任务,Python 定义函数用 def 关键字。

下面是一个打印问候语的简单函数:

def hello():
   print("hello")
   
hello() #函数调用

和 for 或者其他代码块结构一样,函数的定义后跟冒号,以及缩进单位相同的代码块,用来表示函数的内容。

1.2 函数传参

函数是一系列相同动作的组合,所以我们可以向函数传入参数,让它显示不同的结果:

def hello(name):
   print('hello, '+ name)

hello('zhangsan')

接收参数打印结果为:

hello, zhangsan

1)默认参数

Python 传参时,如果没有特殊说明,这些参数都是必传参数。这时如果你不想传参,那函数就会报错,如何避免报错呢?最简单的方法就是给参数设置一个默认参数:

def hello(name="world"):
   print('hello, '+ name)

hello()

如果没有传参,结果就会打印为:

hello, world

注意:默认参数可以有多个,但默认参数只能在必选参数的后面,否则 Python 的解释器会报错。

如果一个函数里有多个默认参数,可以在调用时加上参数名:

def person(name, gender, age=6, city='Beijing'):
   print('name:', name)
   print('gender:', gender)
   print('age:', age)
   print('city:', city)
>>> person('yfx''M'18)
>>> person('yfx''F', city='shenzhen')

如上所示,如果不加参数名,函数会按传参顺序使用这些参数。

2)接收任意数量的参数

当函数的传参个数不确定时,Python 可以用一个 * 参数接收任意数量的位置参数,如:

def avg(first, *rest):
   return (first + sum(rest)) / (1 + len(rest))

avg(12# 1.5
avg(1234# 2.5

此例中,rest 是由除了第一个参数外的所有参数组成的 tuple。

除了接收任意数量的 tuple,我们还可以添加任意数量的 dict,用 ** 表示:

def person(name, age, **attrs):
   print(name, age, attrs)

person('zhangsan'18, {'gender':'Male''job':'student'})

1.3 返回值

函数更多时候是对一段代码的封装,即输入一些参数,经过函数处理后返回一个或一组输出值。Python 中,return 语句可以返回函数调用的返回值。

def add(a, b):
   return a+b
add(1,2#输出求和结果3

1)返回元组

除了返回单个值以为,Python 还可以返回多个值,通过元组的方式呈现:

def point():
   x = 1
   y = 2
   return x, y
point() # (1,2)

和返回单个值一样,返回多个值时也是用 return 语句。

2)返回字典

def build_persion(f_name, l_name, age=0):
   person = {'first': f_name, 'last': l_name}
   if age:
       person['age'] = age
   return person

2. 迭代与生成器

2.1 可迭代对象

Python 中的字符串、列表、元组和字典在获取单个元素时都需要用到迭代:

字符串迭代:

for c in 'abc':
   print(c)
# 打印结果:
a
b
c

字典迭代:

person = {'name':'zhangsan''age':18'gender':'Male'}
for key in person:
   print(key)
打印结果:
name
age
gender

如果要对字典的 value 和 key-value 同时迭代,可以用 values()items()

person = {'name':'zhangsan''age':18, 'gender':'Male'}
for v in person.values():
   print(v)
#打印结果:
zhangsan
18
Male

#通过key-value迭代
for k,v in person.items():
   print(k,v)
#打印结果:
name zhangsan
age 18
gender Male

同时,如果想对列表进行下标迭代,可以用 Python 内置的 enumrate 函数处理:

for i,v in enumerate([4, 5, 6]):
   print(i,v)
#打印结果:
0 4
1 5
2 6

判断是否可迭代

Python 通过 Iterable 判断某个数据结构是否可迭代:

from collections.abc import Iterable
isinstance([1,2,3], Iterable)
# 结果:True

2.2 生成器

1)列表生成式(range)

range(n) 的用法我们已经在 Python入门篇(上)中说过了,这里我们复习一下:

arr = list(range(1,6))
print(arr)
# [1,2,3,4,5]

列表生成式是在一行代码中生成列表的用法,比如我们想生成列表 arr 的平方:

arrs = [x*x for x in arr]
print(arrs)
# [1,4,9,16,25]

还可以通过两层循环遍历两个列表:

comp = [m + n for m in 'abc' for n in '12']
print(comp)
# ['a1', 'a2', 'b1', 'b2', 'c1', 'c2']

2)生成器(generator)

生成器 generator 是 Python 的一个独有特性,是一种随取随用的数据结构。例如:

g = (x*x for x in range(10))
g
#<generator object <genexpr> at 0x1022ef630>

生成器的创建与列表生成式十分类似,不同的是列表生成式用 [] 生成,结果是一个列表;而后者用 () 生成, 结果是一个 generator

next() 函数可以获取 generator 的下一个结果:

next(g) # 0
next(g) # 1
...
next(g) # 81

或者使用 for 循环:

for v in g:
   print(v)

3)函数变为 generator

普通函数会从定义开始直到 return 或者执行完所有代码后结束。

但在 Python 中,如果一个函数定义中包含 yield 关键字,那么这个函数就是一个 generator,调用这种函数就会返回一个 generator

def add(num):
   n = 1
   sum = 0
   while n<=num:
       sum += n
       yield sum
       n += 1
return 'success'

注意,当我们调用这个 generator 函数时,如:add(100),并非立即得出 1+2+...+100 的结果。而是依次返回数字:1、3、6、10...5050:

f = add(100)
next(f) # 1
next(f) # 3
for sum in f:
   print(sum#1、3、6、10...5050

同时,如果用 for 循环获取 generator 的值时,无法获取 return 的返回,此时我们可以捕获StopIteration错误,返回值就包含在 value 里面:

f = add(100)
while True:
   try:
       sum = next(f)
       print('sum:'sum)
   except StopIteration as e:
       print('Generator return value:', e.value)
       break

执行结果:

sum: 1sum: 3...sum:5050Generator return value: success

2.3 迭代器

在 Python 中,可以被 next() 函数调用并不断返回下一个值的对象被称为迭代器:Iterator

和可迭代对象(Iterable)不同,列表、元组、字典、集合、字符串等对象虽然是可迭代的,但是它们没法直接使用 next() 获取下一个值,因此它们不是迭代器。示例:

from collections.abc import Iterable, Iterator
isinstance([1,2,3], Iterable) # True,列表是一个可迭代对象
isinstance([1,2,3], Iterator) # False,列表不是一个迭代器

这时可能有人就蒙逼了,迭代器和可迭代对象有什么本质区别,Python 这两个迭代对象如何区分呢?

我们只需要关注:迭代器对象表示的是一个数据流,可以源源不断地取数据,这个数据流的长度我们在遍历前是不确定的。而可迭代对象可以通过 len() 来获取对象的长度,它们的长度在遍历之前就是确定的。

把可迭代对象变成迭代器,可使用 iter() 方法:

isinstance(iter([1,2,3]), Iterator) # True

3. 面向对象

面向对象是一种设计思想,大部分高级编程语言都采用了这种思想来管理代码结构,Python 也不例外。

对象是一种抽象概念,为了方便描述,面向对象中衍生了两个最重要的实体概念:类(Class)和实例(Instance)。

其中,泛指具有相同特征的集合。比如:动物园中的所有动物,可以抽象为动物类。实例是根据类创建出的一个个具体对象,每个对象都有着相同的属性和行为,即这个类别共同拥有的属性和行为。

比如,动物拥有自己的类别,名字,叫声等属性;拥有自己的跑动,进食方式等。

3.1 类初始化 init

Python 中用 class 来声明类,比如,定义一个动物类:

class Animal(object):
   def __init__(self, kind, name, sound):
       self.name = kind
       self.kind = name
       self.sound = sound

在 Python 中定义类时,必须指定继承一个父类,如果没有合适的父类,就用 object 祖先类。

由于类是所有实例的模板,所以我们在创建实例的时候必须要传入类的基本属性,把它们放入 __init__ 方法里。并且,把传入的属性绑定到 self ,即当前实例上。

比如定义一只小狗旺财:

wc = Animal('旺财''狗''汪汪')
print(wc.name) # 旺财

3.2 基础属性

1)实例的额外属性

和静态语言不同,Python 中同一个对象的实例可能有不同的属性,如:

kitty = Animal('kitty''猫''喵喵')
kitty.age = 1
print(kitty.age) # 1
print(wc.age) # 报错,旺财不存在该属性

2)私有属性

Python 中可以在属性名称之前加两个下划线,声明为私有属性,私有属性不能在类外部访问,如:

class Student(object):
   def __init__(self, name, score):
       self.__name = name
       self.__score = score
   def print_score(self):
       print('%s: %s' % (self.__name, self.__score))

当我们创建实例时,只能通过类里面的方法打印学生的成绩:

s = Student('zhangsan'95)
s.print_score() # zhangsan: 95
s.__score # 'Student' object has no attribute '__score'

3)获取所有属性&方法

Python 中,我们可以用 dir() 来获取类所有的属性和方法,如:

dir(Animal)
['__class__''__delattr__''__dict__''__dir__''__doc__''__eq__''__format__''__ge__''__getattribute__''__gt__''__hash__''__init__''__init_subclass__''__le__''__lt__''__module__''__ne__''__new__''__reduce__''__reduce_ex__''__repr__''__setattr__''__sizeof__''__str__''__subclasshook__''__weakref__']

可以看到有很多我们没主动定义的 __xxx__方法,这些方法在 Python 中是有特殊用途的。比如 __str__ 返回一个实例的描述,我们可以调用 str() 方法,Python 会自动调用 __str__ 方法:

print(str(Animal))
"<class '__main__.Animal'>"
print(Animal) # 相当于调用 Animal.__str__()
<class '__main__.Animal'>

合理打印对象数据

通过以上可知,一个类通过继承祖先 object 类,可以获取很多初始化方法,比如上述打印类的 __str__ 方法。所以,我们还可以重写这些方法:

class Animal(object):
   def __init__(self, kind, name, sound):
       self.name = kind
       self.kind = name
       self.sound = sound
   def __str__(self):
       return 'Animal object(kind:%s) name:%s, sound:%s' % (self.kind, self.name, self.sound)

此时,直接打印对象数据时,就变成了:

>>> k = Animal('kitty''cat''喵喵')
>>> print(k)
Animal object(kind:kitty) name:cat, sound:喵喵

4)测试&新增属性

Python 提供了三个方法,可以操控一个类或对象的属性:

>>> k = Animal('kitty''猫''喵喵')
>>> hasattr(k, 'name'# 对象是否有name属性
True
>>> getattr(k, 'name'# 获取属性name
'kitty'
>>> hasattr(k, 'age')
False
>>> setattr(k, 'age'1# 设置属性age默认值为1
>>> getattr(k, 'age')
1

其中 hasattr 判断对象或类是否存在某个属性,getattr 获取对象或类的属性值,当获取不存在的属性时 Python 会报错;而 setattr 则为对象或类添加属性,并为其设置默认值。

3.3 高级属性

1)__slots__限制类属性

Python 作为一门动态语言,可以在定义好 class 之后给实例绑定任意的属性和方法,如:

k = Animal('kitty''猫''喵喵')
k.age = 1 # 指定猫咪的年龄为1岁

但是,假设我定义了一个类,任何人都可以添加类属性,这样势必会带来管理困难的问题。那我们如何让类保持初始的属性字段呢?

Python 提供了 __slots__ 变量,来限制 class 类的实例属性:

class Animal(object):
   __slots__ = ('kind''name''sound')
   def __init__(self, kind, name, sound):
       self.name = kind
       self.kind = name
       self.sound = sound

这时,如果我们还为实例添加额外的属性,就会报错:

k = Animal('kitty''猫''喵喵')
k.age = 1 # AttributeError: 'Animal' object has no attribute 'age'

2)装饰器@property

为了方便调用,Python 提供了装饰器 @property,可以将类的方法变成属性来调用:

class Animal(object):
   @property
   def kind(self):
       #为了和方法名做区分,在属性前面加一个下划线
       return self._kind
   @kind.setter
   def kind(self, value):
       if not isinstance(value, str):
           raise ValueError('kind must be a str!')
       self._kind = value

调用 @setter 和 @property 装饰器的方法:

k = Animal()
k.kind = 'cat' # OK,相当于调用 k.set_kind('cat')
k.kind # cat,相当于调用 k.get_kind()

特别注意:为了防止属性的方法名和实例变量重名,我们在设置变量时,可以在属性前面加一个下划线

3)__getattr__动态获取属性

当我们调用不存在的方法或属性时,Python 会报错,如:

class Animal(object):
   __slots__ = ('kind''name''sound')
   def __init__(self, kind, name, sound):
       self.name = kind
       self.kind = name
       self.sound = sound
k = Animal('kitty''猫''喵喵')
k.age # 报错,不存在该属性

如何避免这种情况呢?

Python 提供了 __getattr__() 可以动态获取属性,我们修改以下 Animal 类:

class Animal(object):
   __slots__ = ('kind''name''sound')
   def __init__(self, kind, name, sound):
       self.name = kind
       self.kind = name
       self.sound = sound
   def __getattr__(self, attr):
       if attr == 'age':
           return 18

k = Animal('kitty''猫''喵喵')
k.age # 18

这样,获取不存在的属性时,我们也可以使 Python 不报错了。

除此之外,__getattr__ 最大的用途在于,我们可以动态获取用户的请求地址。比如,根据请求地址实现一个链式调用:

class Chain(object):
   def __init__(self, path=''):
       self._path = path

   def __getattr__(self, path):
       return Chain('%s/%s' % (self._path, path))

   def __str__(self):
       return self._path

打印对象的调用路径:

print(Chain().active.user.list)
'/active/user/list'

这种方式可以实现 SDK 根据 URL 的动态变化而切换,而无需新增过多的接口监听。

3.4 多重继承

不同于 Java 等编程语言只能单继承,Python 一个类可以有多个父类,即多重继承。

多重继承在自然界中很常见,归因于不同的分类方式。比如动物根据类别可以分为哺乳类和鸟类,也可以根据运动方式分为天上飞的,水里游的,陆上走的。

我们可以声明一个跑动类动物:

class Runnable(Animal):
   pass

一个哺乳类动物:

class Mammal(Animal):
   pass

一个跑动的哺乳类动物:

class Cat(Runnable, Mammal):
   pass

继承了多个父类的子类,可以同时获得多个父类的功能。

3.5 枚举类

在 Python 中,常量定义一般用大写变量声明,比如:

SUN = 0
MON = 1
...
Sat = 6

而枚举类则是将相同属性的常量放在一个类里面,如:

from enum import Enum, unique

@unique
class Weekday(Enum):
   Sun = 0
   Mon = 1
   Tue = 2
   Wed = 3
   Thu = 4
   Fri = 5
   Sat = 6

其中,装饰器 unique 可以避免枚举 key 重复。

将一组具有相同特征的常量放在枚举类中,有很多好处。第一个好处就是遍历很方便:

for name, value in Weekday.__members__.items():
   print(name, '->', value)
# 打印结果:
Sun -> Weekday.Sun
Mon -> Weekday.Mon
Tue -> Weekday.Tue
Wed -> Weekday.Wed
Thu -> Weekday.Thu
Fri -> Weekday.Fri
Sat -> Weekday.Sat

还可以随取随用:

print(Weekday.Tue) # Weekday.Tue
print(Weekday.Tue.value) # 2
print(Weekday(1)) # Weekday.Mon

4. 错误处理

当我们在编写业务代码时,由于很多不可控的原因,代码可能会异常运行导致系统崩溃。

此时,对内来说由于资源未释放,报错堆栈不明显等原因,会让系统的稳定性受到挑战;对外来说,如果用户的请求导致系统崩溃,用户的体验会非常差。

所以,为了系统的健壮性考虑,我们必须要做错误处理。

4.1 try/except/finally

和 Java 语言一样,Python 内置了一套 try...except...finally 的错误处理方式,可以在代码中方便地获取异常信息。假设我们编写了一个除法函数:

func div(a, b):
   try:
       print('try...')
       r = a / b # 假设b为0,会抛出异常
       print('result:', r)
   except ZeroDivisionError as e:
       print('except:', e)
   finally:
       print('finally...')
   print('END')

当某些代码可能会出错时,我们就可以用try来运行这段代码,即试着运行一下。

如果执行出错,则后续代码不会继续执行,而是直接跳转至错误处理代码,即except语句块,捕获和处理异常。

执行完except后,如果有finally语句块,则执行finally语句块,一般用作资源释放,比如数据库连接、文件资源释放等。

Python所有的错误都是从BaseException类派生的,常见的错误类型和继承关系可以看官方文档:

docs.python.org/3/library/e…

4.2 自定义错误

如上述所示,为了防止除数为 0,我们还可以在入口处校验,如果除数是否为 0,则用 raise 主动抛出异常:

func div(a, b):
   try:
       print('try...')
       if b==0:
           raise ZeroDivisionError('divider cannot be 0')
       r = a / b # 假设b为0,会抛出异常
       print('result:', r)
   except ZeroDivisionError as e:
       print('except:', e)
   finally:
       print('finally...')
   print('END')

这样做的好处是我们把代码异常变成了校验异常,可以定制化我们的异常信息,抛出自定义的 Error 类。

这里肯定有同学会有疑问了,那我在编写代码时,记不住这么多 Error 类怎么办?

其实很简单,Python 提供这么多错误类只是方便细化后使用,我们也可以用一些基本类进行错误抛出:

func div(a, b):
   try:
       if b == 0:
           raise ValueError('divider cannot be 0')
   except Except as e: #可以捕获所有的Error
       print('except:', e)

这样,我们就可以捕获到所有的异常信息,进行错误处理了。

5. 小结

对于一个之前写过 Java 和 Go 的人来说,Python 上手其实还蛮快的,学了不到一个周就开始写项目了。但很多时候也会犯一些基础错误:比如 Python 作为一门动态语言,做声明时需要特别注意,拿一个实际场景对比。

在 Go 语言下:

a := "123"
a = 1 # 不允许赋值

而 Python 里面这样的赋值是允许的,所以在命名上尤其需要注意是否重名。除此之外,还有很多入门者会踩的坑,后续有时间会整理到 GZH 文章里。

但用了一段时间的 Python,发现其在数据和文件处理等方面非常强大,在 Go 里面需要反复转换的格式,在 Python 里可能仅需要一两行代码就可以搞定了,妥妥懒人福音:)

最后,求一波关注,xin猿意码 GZH 主要输出一些编程知识、面试经验、互联网行业动态相关的内容,作者具有多年互联网大中小厂后台开发经验,可一起交流学习。

​敬请期待《Python 入门篇(下)》!