Python 进阶教程笔记

面向对象编程

一、什么是面向对象编程

  • 程序设计的范式
  • 抽象并建立对象模型
  • 程序是不同对象相互调用的逻辑

二、Python类的定义与实例化

在Python中,通过class关键字定义一个类,比如我们需要定义一个人的类。按照 Python 的编程习惯,类名以大写字母开头。因此可以这样定义:

class Person:  pass

注意,在这个Person类的定义里面,并没有继承任何类,除了这样定义以外,还可以有以下两种定义方式。

class Person(): pass  
class Person(object):  pass

这三种情况有什么区别呢?在Python3中,是没有区别的,但是在Python2中,则有一定的区别。

在Python2中,对于第一种定义的方法,Person类只有有限的几个内建函数'__doc__', '__module__', 'name',而对于第二种、第三种定义的方法,则会继承Python object对象的更多的内建函数,可以更便捷的操作对象。这是Python2版本的差异。在Python3中,我们只需要知道这三种方式都可以定义一个类即可。

定义了类之后,就可以对类进行实例化了,实例化是指,把抽象的类,赋予实物的过程。比如,定义好Person这个类后,就可以实例化多个Person出来了。 创建实例使用类名+(),类似函数调用的形式创建:

class Person(object):  pass 
xiaohong = Person()
xiaoming = Person()

任务

请练习定义一个动物类,并创建出两个实例dog, cat,打印实例,再比较两个实例是否相等。

要打印实例,直接使用print语句;

要比较两个实例是否相等,用==操作符。

参考答案
class Animal(object):  
	pass

dog = Animal()
cat = Animal()
print(dog) print(cat) print(dog == cat)

三、实例属性的定义

虽然前面我们已经通过Person类创建出xiaoming、xiaohong等实例,但是这些实例看上去并没有任何区别。在现实世界中,一个人拥有名字、性别、年龄等等的信息,在Python中,可以通过以下的方式赋予实例这些属性,并且把这些属性打印出来。

xiaohong.name = 'xiaohong'
xiaohong.sex = 'girl'
xiaohong.age = 13

print(xiaohong.name)
print(xiaohong.sex)
print(xiaohong.age)

除此以外,这些属性也可以和普通变量一样进行运算。比如xiaohong长大了一岁:

xiaohong.age = xiaohong.age + 1

任务

请定义一个动物类,并创建出两个实例dog, cat,分别赋予不同的名字和年龄并打印出来。

参考答案
class Animal(object):
    pass

dog = Animal()
cat = Animal()
dog.name = 'wangwang'
dog.age = 1
cat.name = 'mimi'
cat.age = 3

四、实例属性的初始化

通过前面的方式定义一个实例的属性非常方便,但也有一些问题。 首先,如果定义属性的过程中使用了不同的属性名字,比如性别,前者使用了sex,后者使用了gender,那对于一个类的不同实例,存储一个信息就用了两个不同的属性,在后面将会难以维护。 其次,名字、性别、年龄等等,都是人的基本信息,在抽象一个类的时候,理应包含这些信息。 在定义 Person 类时,可以为Person类添加一个特殊的__init__()方法,当创建实例时,__init__()方法被自动调用,我们就能在此为每个实例都统一加上以下属性:

class Person(object):
    def __init__(self, name, sex, age):
        self.name = name
        self.sex = sex
        self.age = age

需要注意的是,__init__() 方法的第一个参数必须是 self(也可以用别的名字,但建议使用习惯用法),后续参数则可以自由指定,和定义函数没有任何区别。 定义类后,就可以相应的实例化对象了,需要注意的是,在实例化的时候,需要提供除self以外的所有参数。

xiaoming = Person('Xiao Ming', 'boy', 13)
xiaohong = Person('Xiao Hong', 'girl', 14)

而访问这些属性的方式和之前的一样:

print(xiaohong.name)
print(xiaohong.sex)
print(xiaohong.age)
# 但当访问不存在的属性时,依然会报错
print(xiaohong.birth)

要特别注意的是,初学者定义__init__()方法常常忘记了 self 参数,比如如下的定义:

class Person(object):
    def __init__(name, sex, age):
        pass

这种情况下,如果还是如下实例化,将会报错。

xiaoming = Person('Xiao Ming', 'boy', 13)
xiaohong = Person('Xiao Hong', 'girl', 14)

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: __init__() takes 3 positional arguments but 4 were given

任务

请定义一个动物类,抽象出名字、年龄两个属性,并实例化两个实例dog, cat

参考答案
class Animal(object):
    def __init__(self, name, age):
        self.name = name
        self.age = age

dog = Animal('wangwang', 1)
cat = Animal('mimi', 3)
print(dog.name)
print(dog.age)
print(cat.name)
print(cat.age)

五、类属性

类和实例对象是有区别的,类是抽象,是模板,而实例则是根据类创建的对象,比如类:动物,只是一个抽象,并没有动物的详细信息,而猫、狗等,则是具体的动物,是类的对象。

在前面,实例对象绑定的属性只属于这个实例,绑定在一个实例上的属性不会影响其它实例;同样的,类也可以绑定属性,但是类的属性不属于任何一个对象,而是属于这个类。如果在类上绑定一个属性,则所有实例都可以访问类的属性,并且,所有实例访问的类属性都是同一个!也就是说,实例属性每个实例各自拥有,互相独立,而类属性有且只有一份。

定义类属性可以直接在 class 中定义,比如在前面的Animal类中,加入地域的类属性:

class Animal(object):
    localtion = 'Asia'
    def __init__(self, name, age):
        self.name = name
        self.age = age

在上面的代码中,localtion就是属于Animal这个类的类属性,此后,通过Animal()实例化的所有对象,都可以访问到localtion,并且得到唯一的结果。

dog = Animal('wangwang', 1)
cat = Animal('mimi', 3)
print(dog.localtion) # ==> Asia
print(cat.localtion) # ==> Asia
# 类属性,也可以通过类名直接访问
print(Animal.localtion) # ==> Asia

类属性也是可以动态添加和修改的,需要注意的是,因为类属性只有一份,所以改变了,所有实例可以访问到的类属性都会变更:

Animal.localtion = 'Africa'
print(cat.localtion) # ==>Africa
print(dog.localtion) # ==>Africa

任务

请给 Animal类添加一个类属性 count,每创建一个实例,count 属性就加 1,这样就可以统计出一共创建了多少个 Animal的实例。

参考答案
class Animal(object):
    count = 0
    def __init__(self, name, age):
        self.name = name
        self.age = age
        Animal.count += 1

dog = Animal('wangwang', 1)
print(Animal.count)
cat = Animal('mimi', 3)
print(Animal.count)
pig = Animal('panpan', 1)
print(Animal.count)

六、类属性和实例属性的优先级

可以看到,属性可以分为类属性和实例属性,那么问题就来了,如果类属性和实例属性名字相同时,会怎么样,这就涉及Python中类属性和实例属性的优先级的问题了。

我们可以做一个实验,在前面类定义的基础上,在实例属性中,也初始化一个localtion的属性。

class Animal(object):
    localtion = 'Asia'
    def __init__(self, name, age, localtion):
        self.name = name
        self.age = age
        self.localtion = localtion

接着我们初始化两个实例,并把localtion打印出来。

dog = Animal('wangwang', 1, 'GuangDong')
cat = Animal('mimi', 3, 'ChongQing')
print(dog.localtion) # ==> GuangDong
print(cat.localtion) # ==> ChongQing
print(Animal.localtion) # ==> Asia

可见,在类属性和实例属性同时存在的情况下,实例属性的优先级是要高于类属性的,在操作实例的时候,优先是操作实例的属性。

另外,当实例没有和类同名的时候,通过实例对象,依然可以访问到类属性。

class Animal(object):
    localtion = 'Asia'
    def __init__(self, name, age):
        self.name = name
        self.age = age

cat = Animal('mimi', 3)
print(cat.localtion) # ==> Asia

那通过实例,可不可以修改类属性呢?我们来尝试一下:

cat.localtion = 'Africa'
print(Animal.localtion) # ==> Asia

这里依然打印了Asia,可见通过实例是无法修改类的属性的,事实上,通过实例方法修改类属性,只是给实例绑定了一个对应的实例属性:

# 新增的实例属性
print(cat.localtion) # ==> Africa

因此,需要特别注意,尽量不要通过实例来修改类属性,否则很容易引发意想不到的错误。

任务

请把上节的 Animal类属性 count 改为 __count,再试试能否从实例和类访问该属性。

参考答案
把count改为私有__count,这样实例变量在外部无法修改__count
class Animal(object):
    __count = 0
    def __init__(self, name):
        Animal.__count = Animal.__count + 1
        self.name = name
        print(Animal.__count)

p1 = Animal('Cat')
p2 = Animal('Dog')

print(Animal.__count)

七、访问限制

并不是所有的属性都可以被外部访问的,这种不能被外部访问的属性称为私有属性。私有属性是以双下划线'__'开头的属性。

# 类私有属性
class Animal(object):
    __localtion = 'Asia'

print(Animal.__localtion)

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: type object 'Animal' has no attribute '__localtion'
# 实例私有属性
class Animal(object):
    def __init__(self, name, age, localtion):
        self.name = name
        self.age = age
        self.__localtion = localtion

dog = Animal('wangwang', 1, 'GuangDong')
print(dog.name) # ==> wangwang
print(dog.age) # ==> 1
print(dog.__localtion)

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Animal' object has no attribute '__localtion'

在外部访问私有属性将会抛出异常,提示没有这个属性。 虽然私有属性无法从外部访问,但是,从类的内部是可以访问的。私有属性是为了保护类或实例属性不被外部污染而设计的。

任务

请给Animal类的__init__方法中添加name和age参数,并把age绑定到__age属性上,看看外部是否能访问到。

参考答案
class Animal(object):
    def __init__(self, name, age):
        self.name = name
        self.__age = age

cat = Animal('Kitty', '3')

print(cat.name)
print(cat.__age)

八、定义实例方法

上面提到,私有属性没有办法从外部访问,只能在类的内部操作;那如果外部需要操作私有属性怎么办?这个时候可以通过定义类或者实例的方法来操作私有属性,本节课先来介绍实例方法。

实例的方法指的就是在类中定义的函数,实例方法的第一个参数永远都是self,self是一个引用,指向调用该方法的实例对象本身,除此以外,其他参数和普通函数是完全一样的。

class Person(object):

    def __init__(self, name):
        self.__name = name

    def get_name(self):
        return self.__name

在上面的定义,name是实例的私有属性,从外部是无法访问的,而get_name(self) 就是一个实例方法,在实例方法里面是可以操作私有属性的,注意,它的第一个参数是self。

另外,__init__(self, name)其实也可看做是一个特殊的实例方法。

通过定义get_name(self)方法,在外部就可以通过这个方法访问私有属性了。

p = Person('Alice')
print(p.get_name()) # ==> Alice

注意,在外部调用实例方法时,是不需要显式传递self参数的。

另外,通过定义实例方法来操作私有属性的这种方法是推荐的,这种数据封装的形式除了能保护内部数据一致性外,还可以简化外部调用的难度。 当然,实例方法并不仅仅是为私有属性服务的,我们可以把和类的实例有关的操作都抽象成实例方法,比如:打印实例的详细信息等等。

class Animal(object):
    def __init__(self, name, age, localtion):
        self.name = name
        self.age = age
        self.localtion = localtion

    def get_info(self):
        return 'name = {}, age = {}, localtion = {}'.format(self.name, self.age, self.localtion)

dog = Animal('wangwang', 1, 'GuangDong')
print(dog.get_info())

任务

把Animal类的age、name、localtion定义成私有属性,并定义对应的方法修改和获取他们的值。

参考答案
class Animal(object):
    def __init__(self, name, age, localtion):
        self.__name = name
        self.__age = age
        self.__localtion = localtion

    def set_name(self, name):
        self.__name = name

    def get_name(self):
        return self.__name

    def set_age(self, age):
        self.__age = age

    def get_age(self):
        return self.__age

    def set_localtion(self, localtion):
        self.__localtion =localtion

    def get_localtion(self):
        return self.__localtion

九、定义类方法

在上面,为了操作实例对象的私有属性,我们定义了实例方法;同样的,如果需要需要操作类的私有属性,则应该定义类的方法。

默认的,在class中定义的全部是实例方法,实例方法第一个参数 self 是实例本身。

要在class中定义类方法,需要这么写:

class Animal(object):
    __localtion = 'Asia'
    def __init__(self, name, age):
        self.name = name
        self.age = age

    @classmethod
    def set_localtion(cls, localtion):
        cls.__localtion = localtion

    @classmethod
    def get_localtion(cls):
        return cls.__localtion

print(Animal.get_localtion()) # ==> Asia
Animal.set_localtion('Afica')
print(Animal.get_localtion()) # ==> Africa

和实例方法不同的是,这里有两点需要特别注意:

  1. 类方法需要使用@classmethod来标记为类方法,否则定义的还是实例方法
  2. 类方法的第一个参数将传入类本身,通常将参数名命名为 cls,上面的 cls.__localtion 实际上相当于Animal.__localtion。

因为是在类上调用,而非实例上调用,因此类方法无法获得任何实例变量,只能获得类的引用。

任务

如果将类属性count改为私有属性__count,则外部无法获取__count,但是可以通过一个类方法获取,请编写类方法获得__count值。

参考答案

注意类方法需要添加 @classmethod

class Animal(object):
    __localtion = 'Asia'
    __count = 0
    def __init__(self, name, age):
        self.name = name
        self.age = age
        Animal.__count += 1

    @classmethod
    def get_count(cls):
        return cls.__count

dog = Animal('wangwang', 1)
cat = Animal('mimi', 3)
pig = Animal('panpan', 1)
count = Animal.get_count()
print(count)

Python进阶教程笔记(二)类的继承、多态

2020-10-302020-10-30 11:42:23阅读 1660

一、什么是继承

  • 程序设计的范式
  • 抽象并建立对象模型
  • 程序是不同对象相互调用的逻辑

二、继承的优点

  • 新类不需要重头编写
  • 继承父类所有的属性、功能
  • 子类只需要实现缺少的新功能

三、继承类

对人类的抽象可以定义为Person类,而学生、老师等,也都是人类,所以,在Python当中,如果定义学生Student的类,可以继承Person类。

class Person(object):
    def __init__(self, name, gender):
        self.name = name
        self.gender = gender

接着定义Student类,在定义Student类的时候,由于继承了Person类,所以Student类自动拥有name、gender属性,因此,在定义Student类的时候,只需要把额外的属性加上即可。

class Student(Person):
    def __init__(self, name, gender, score):
        super(Student, self).__init__(name, gender)
        self.score = score

student = Student('Alice', 'girl', 100)
print(student.name) # ==> Alice
print(student.gender) # ==> girl
print(student.score) # ==> 100

在定义继承类的时候,有几点是需要注意的:

  1. class Student()定义的时候,需要在括号内写明继承的类Person
  2. __init__()方法,需要调用super(Student, self).__init__(name, gender),来初始化从父类继承过来的属性

任务

请参考Student类,编写Teacher类,老师拥有任教某个科目的属性。

参考答案
class Person(object):
    def __init__(self, name, gender):
        self.name = name
        self.gender = gender

class Teacher(Person):
    def __init__(self, name, gender, course):
        super(Teacher,self).__init__(name,gender)
        self.course = course

teacher = Teacher('Alice', 'Female', 'English')
print(teacher.name)
print(teacher.gender)
print(teacher.course)

四、判断类型

随着我们学习步伐的前进,我们的程序会出现越来越多的类型,有我们自己定义的类,也有Python自有的str、list、dict等,他们的本质都是都是Python中的一种数据类型,这时有必要去判断数据的类型,通过函数isinstance()可以判断一个变量的类型。

class Person(object):
    def __init__(self, name, gender):
        self.name = name
        self.gender = gender

class Student(Person):
    def __init__(self, name, gender, score):
        super(Student, self).__init__(name, gender)
        self.score = score

class Teacher(Person):
    def __init__(self, name, gender, course):
        super(Teacher, self).__init__(name, gender)
        self.course = course

p = Person('Tim', 'Male')
s = Student('Bob', 'Male', 88)
t = Teacher('Alice', 'Female', 'English')

当我们拿到变量 p、s、t 时,可以使用 isinstance 判断类型:

>>> isinstance(p, Person)
True # p是Person类型
>>> isinstance(p, Student)
False # p不是Student类型
>>> isinstance(p, Teacher)
False # p不是Teacher类型

这说明在继承链上,一个父类的实例不能是子类类型,因为子类比父类多了一些属性和方法。 我们再考察 s:

>>> isinstance(s, Person)
True # s是Person类型
>>> isinstance(s, Student)
True # s是Student类型
>>> isinstance(s, Teacher)
False # s不是Teacher类型

s 是Student类型,不是Teacher类型,这很容易理解。但是,s 也是Person类型,因为Student继承自Person,虽然它比Person多了一些属性和方法,但是,把 s 看成Person的实例也是可以的。

这说明在一条继承链上,一个实例可以看成它本身的类型,也可以看成它父类的类型。

isinstance也可以用于Python自有数据类型的判断。

s = 'this is a string.'
n = 10
isinstance(s, int) # ==> False
isinstance(n, str) # ==> False

任务

请根据继承链的类型转换,依次思考 t 是否是 Person,Student,Teacher,object 类型,并使用isinstance()判断来验证您的答案。

参考答案
  1. 使用isinstance()来进行判断

  2. 使用print()函数打印输出判断结果

class Person(object):
    def __init__(self, name, gender):
        self.name = name
        self.gender = gender

class Student(Person):
    def __init__(self, name, gender, score):
        super(Student, self).__init__(name, gender)
        self.score = score

class Teacher(Person):
    def __init__(self, name, gender, course):
        super(Teacher, self).__init__(name, gender)
        self.course = course

p = Person('Tim', 'Male')
s = Student('Bob', 'Male', 88)
t = Teacher('Alice', 'Female', 'English')
isinstance(t, Person)
isinstance(t, Student)
isinstance(t, Teacher)
isinstance(t, object)

五、多态

类具有继承关系,并且子类类型可以向上转型看做父类类型,如果我们从 Person 派生出 Student和Teacher ,并都写了一个who() 方法:

class Person(object):
    def __init__(self, name, gender):
        self.name = name
        self.gender = gender
    def who(self):
        return 'I am a Person, my name is %s' % self.name

class Student(Person):
    def __init__(self, name, gender, score):
        super(Student, self).__init__(name, gender)
        self.score = score
    def who(self):
        return 'I am a Student, my name is %s' % self.name

class Teacher(Person):
    def __init__(self, name, gender, course):
        super(Teacher, self).__init__(name, gender)
        self.course = course
    def who(self):
        return 'I am a Teacher, my name is %s' % self.name

接着,我们分别把不同类型的who()函数结果打印出来:

p = Person('Tim', 'Male')
s = Student('Bob', 'Male', 88)
t = Teacher('Alice', 'Female', 'English')

运行结果:

I am a Person, my name is Tim
I am a Student, my name is Bob
I am a Teacher, my name is Alice

这种行为称为多态。从定义上来讲,Student和Teacher都拥有来自父类Person继承的who()方法,以及自己定义的who()方法。但是在实际调用的时候,会首先查找自身的定义,如果自身有定义,则优先使用自己定义的函数;如果没有定义,则顺着继承链向上找。

class Boss(Person):
    def __init__(self, name, gender,company):
        super(Boss, self).__init__(name, gender)
        self.company = company

b = Boss('Bob', 'Male', 'Alibaba')
b.who() # ==> I am a Person, my name is Bob

在Boss的定义类,没有定义who方法,所以会顺着继承链向上找到父类的who方法并且调用。

Python中的多重继承

除了从一个父类继承外,Python允许从多个父类继承,称为多重继承。多重继承和单继承没有特别大的差异,只是在括号内加入多个需要继承的类的名字即可。

class A(object):
    def __init__(self, a):
        print ('init A...')
        self.a = a

class B(A):
    def __init__(self, a):
        super(B, self).__init__(a)
        print ('init B...')

class C(A):
    def __init__(self, a):
        super(C, self).__init__(a)
        print ('init C...')

class D(B, C):
    def __init__(self, a):
        super(D, self).__init__(a)
        print ('init D...')

多重继承的继承链就不是一棵树了,它像这样:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Kqbi9LT5-1651453818300)(https://ask.qcloudimg.com/http-save/yehe-1217896/lty3oeb5zi.png?imageView2/2/w/1620)]

从上图可知,A类被继承了连词,那么A的__init__()方法,是否会被调用两次呢?

d = D('d')
init A...
init C...
init B...
init D...

实践证明,在多重继承里,A虽然被继承了两次,但是__init__()的方法只调用一次。

多重继承的目的是从两种继承树中分别选择并继承出子类,以便组合功能使用。

举个例子,Python的网络服务器有TCPServer、UDPServer、UnixStreamServer、UnixDatagramServer,而服务器运行模式有 多进程ForkingMixin 和 多线程ThreadingMixin两种。

要创建多进程模式的 TCPServer:

class MyTCPServer(TCPServer, ForkingMixin)
    pass

要创建多线程模式的 UDPServer:

class MyUDPServer(UDPServer, ThreadingMixin):
    pass

如果没有多重继承,要实现上述所有可能的组合需要 4x2=8 个子类。

任务

已知类Student、Teacher继承Person类,技能类BasketballMixin、FootballMixin继承SkillMixin类,请通过多重继承,分别定义“会打篮球的学生”和“会踢足球的老师”。

参考答案
class Person(object):
    pass

class Student(Person):
    pass

class Teacher(Person):
    pass

class SkillMixin(object):
    pass

class BasketballMixin(SkillMixin):
    def skill(self):
        return 'basketball'

class FootballMixin(SkillMixin):
    def skill(self):
        return 'football'

class BStudent(BasketballMixin, Student):
    pass

class FTeacher(FootballMixin, Teacher):
    pass

s = BStudent()
print(s.skill())

t = FTeacher()
print(t.skill())

六、获取对象信息

在前面,我们通过isinstance()方法,可以判断一个对象是否是某个类型,从某种意义上来讲,通过isinstance()方法,我们获取到了一个对象的一些信息,那有没有别的方法可以获取到对象更多的信息呢? 通过type()函数,可以获得变量的类型。

n = 1
s = 'this is a string'
type(n) # ==> <class 'int'>
type(s) # ==> <class 'str'>
class Person(object):
    def __init__(self, name, gender):
        self.name = name
        self.gender = gender

class Student(Person):
    def __init__(self, name, gender, score):
        super(Student, self).__init__(name, gender)
        self.score = score

p = Person('Alice', 'Female')
s = Student('Bob', 'Male', 100)
type(p) # ==> <class '__main__.Person'>
type(s) # ==> <class '__main__.Student'>

通过dir()方法,可以获取变量的所有属性:

>>> n = 1
>>> dir(n)
['__abs__', '__add__', '__and__', '__bool__', '__ceil__', '__class__', '__delattr__', '__dir__', '__divmod__', '__doc__', '__eq__', '__float__', ...]
>>> s = 'this is a string'
>>> dir(s)
['__add__', '__class__', '__contains__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__getnewargs__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__iter__', '__le__', '__len__', '__lt__', '__mod__', '__mul__', '__ne__', '__new__', ...]

在dir列出的属性中,有很多是以下划线开头和结尾的,这些都是特殊的方法,称为内建方法,在后面,我们还会学习这些方法。 而对于自定义对象:

class Person(object):
    def __init__(self, name, gender):
        self.name = name
        self.gender = gender
    def who(self):
        return 'I am a Person, my name is {}'.format(self.name)

p = Person('Alice', 'Female')
dir(p)

['__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__', 'gender', 'name', 'who']

对于实例变量,dir()返回所有实例属性,包括__class__这类有特殊意义的属性。注意到方法who也是p的一个属性。

dir()返回的属性是字符串列表,如果已知一个属性名称,要获取或者设置对象的属性,就需要用 getattr() 和 setattr( )函数了。

>>> getattr(p, 'name') # 获取name属性
'Alice'
>>> setattr(p, 'name', 'Adam') # 设置新的name属性
>>> s.name
'Adam'
>>> getattr(s, 'age') # 获取age属性,但是属性不存在,报错:
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Person' object has no attribute 'age'
>>> getattr(s, 'age', 20) # 获取age属性,如果属性不存在,就返回默认值20:
20

任务

对于Person类的定义:

class Person(object):
    def __init__(self, name, gender):
        self.name = name
        self.gender = gender

希望除了namegender外,可以提供任意额外的关键字参数,并绑定到实例,请修改 Person 的__init__()定义,完成该功能。

参考答案
提示:结合在入门课程学习的向函数传递任意关键字参数的方法以及刚刚学习的setattr()方法。
class Person(object):
    def __init__(self, name, gender, **kw):
        self.name = name
        self.gender = gender
        for k, v in kw.items():
            setattr(self, k, v)

p = Person('Bob', 'Male', age=18, course='Python')
print(p.age)
print(p.course)

Python进阶教程笔记(三)类的特殊方法

一、什么是特殊方法

  • 双下划线开头
  • 双下划线结尾
  • 每个Python对象都拥有特殊方法
  • 常见特殊方法
__str__(), __add__(), __sub__(), __mul__(), __truediv__(), __len__(),
__new__(), __init__(), __del__(), __repr__(), __bytes__(), __format__(),
__lt__(), __le__(), __eq__(), __ne__(), __gt__(), __ge__(), __hash__(),
__bool__(), __dir__(), __set__(), __call__(), __slots__(), ...

二、类的__str__ 和 __repr__方法

对于Python的内建对象,比如int、dict、list等,通过str()方法,可以把这些对象转换为字符串对象输出。

num = 12
str(num) # ==> '12'
d = {
    
    1: 1, 2: 2}
str(d) # ==> '{1: 1, 2: 2}'
l = [1,2,3,4,5]
str(l) # ==> '[1, 2, 3, 4, 5]'

对于自定义对象,通过str()方法,同样可以得到对象所对应的字符串结果,只不过结果会有些难理解。

class Person:
    pass

bob = Person()
str(bob) # ==> '<__main__.Person object at 0x7fc77b859c50>'

<__main__.Person object at 0x7fc77b859c50>这个结果其实是Animal的实例cat在内存中的地址,这是相当难以理解的,不过引发思考的是,通过str()打印的数据,是怎么来的呢?

这其实是对象的内建方法__str__返回的。 通过dir()方法,我们可以把对象的所有方法打印出来。

>>> dir(list)
['__add__', '__class__', '__contains__', '__delattr__', '__delitem__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__gt__', '__hash__', '__iadd__', '__imul__', '__init__', '__init_subclass__', '__iter__', '__le__', '__len__', '__lt__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__reversed__', '__rmul__', '__setattr__', '__setitem__', '__sizeof__', '__str__', '__subclasshook__', 'append', 'clear', 'copy', 'count', 'extend', 'index', 'insert', 'pop', 'remove', 'reverse', 'sort']

可以看到,int、dict、list等的内建对象都实现了自己的__str__()方法,可以把相应的字符串返回,如果我们的类也想把容易理解的字符串输出的话,那么我们也需要实现类的__str__()方法。

class Person(object):
    def __init__(self, name, gender):
        self.name = name
        self.gender = gender
    def __str__(self):
        return 'name: {}, gender: {}'.format(self.name, self.gender)

bob = Person('Bob', 'Male')
str(bob) # ==> 'name: Bob, gender: Male'

但是,对于直接在终端输入变量bob,得到的依然是这样结果。

>>> bob
<__main__.Person object at 0x7fc77b859cc0>

而对于int、list等的对象,直接输入变量也可得到可读的结果。

>>> num = 12
>>> str(num)
'12'
>>> d = {
    
    1: 1, 2: 2}
>>> d
{
    
    1: 1, 2: 2}

__str__()函数似乎没有在自定义类Person中生效,这是为什么呢?

这是因为 Python 定义了__str()____repr__()两种方法,__str()__用于显示给用户,而__repr__()用于显示给开发人员,当使用str()时,实际调用的是__str__()方法,而直接输入变量,调用的是__repr__()方法。

class Person(object):
    def __init__(self, name, gender):
        self.name = name
        self.gender = gender
    def __str__(self):
        return 'name: {}, gender: {}'.format(self.name, self.gender)
    def __repr__(self):
        return 'name: {}, gender: {}'.format(self.name, self.gender)

bob = Person('Bob', 'Male')
str(bob) # ==> 'name: Bob, gender: Male'
>>> bob
'name: Bob, gender: Male'

任务

请给Student 类定义__str__和__repr__方法,使得能打印出’Student: name, gender, score’。

参考答案
class Person(object):
    def __init__(self, name, gender):
        self.name = name
        self.gender = gender

class Student(Person):
    def __init__(self, name, gender, score):
        super(Student, self).__init__(name, gender)
        self.score = score
    def __str__(self):
        return 'Student: {}, {}, {}'.format(self.name, self.gender, self.score)
    def __repr__(self):
        return 'Student: {}, {}, {}'.format(self.name, self.gender, self.score)

s = Student('Bob', 'Male', 88)
print(s)

三、类的__len__方法

对于列表List或者元组Tuple,通过内建方法len(),可以得出列表或者元组中元素的个数。如果一个类表现得像一个list,想使用len()函数来获取元素个数时,则需要实现len()方法。

比如我们实现一个班级Class的类,初始化把班级的同学名字列表传进去,希望len()函数可以返回班级同学的数量时,可以这样实现。

class Class:
    def __init__(self, students):
        self.students = students
    def __len__(self):
        return len(self.students)

students = ['Alice', 'Bob', 'Candy']
class_ = Class(students)
len(class_) # ==> 3

通过自定义__len__()方法,可以让len()函数返回相关的结果,如果没有定义__len__()方法的类使用len()函数获取长度时,将会引起异常。

class Class:
    def __init__(self, students):
        self.students = students

class_ = Class(students)
len(class_)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: object of type 'Class' has no len()

任务

斐波那契数列是由 0, 1, 1, 2, 3, 5, 8…构成。

请编写一个Fib类,Fib(10)表示数列的前10个元素,print Fib(10) 可以打印出数列的前 10 个元素,len(Fib(10))可以正确返回数列的个数10。

参考答案
class Fib(object):
    def __init__(self, num):
        self.res = []
        self.num = num
        a = 0
        b = 1
        for x in range(num):
            self.res.append(a)
            a, b = b, a + b

    def __str__(self):
        return str(self.res)

    def __len__(self):
        return self.num

f = Fib(10)
print(f)
print(len(f))

四、类的数学运算

事实上,Python很多的操作都是通过内建函数来实现的,比如最熟悉的加减乘除,都是通过内建函数来实现的,分别是__add__、__sub__、__mul__、__truediv__。因此,只要我们的自定义类实现了相关的内建函数,我们的类对象,也可以做到加减乘除。

对于有理数,我们可以使用Rational类来表示:

class Rational(object):
    def __init__(self, p, q):
        self.p = p
        self.q = q

其中,p、q 都是整数,表示有理数 p/q。 如果要让Rational进行加法运算,需要正确实现__add__

class Rational(object):
    def __init__(self, p, q):
        self.p = p
        self.q = q
    def __add__(self, r):
        return Rational(self.p * r.q + self.q * r.p, self.q * r.q)
    def __str__(self):
        return '{}/{}'.format(self.p, self.q)

定义好后,就可以尝试一下有理数的加法了:

>>> r1 = Rational(1, 2)
>>> r2 = Rational(2, 3)
>>> print(r1 + r2)
7/6

需要注意__add__()函数,它有一个参数,表示的是运算的第二个操作数,比如:r1 + r2,那么在add()方法中的参数,r指的就是r2,这个参数是运算符重载的时候传递的。

另外,细心的同学可能注意到了,相比加减乘的特殊方法,除法的特殊方法名字较长__truediv__,并且含有true这样的描述,这其实和Python除法是有关系的。

Python的除法可以分为地板除(你没看错,就是地板)和普通除法,地板除的特殊方法是__floordiv__,普通除法是__truediv__

地板除法和普通除法不一样,地板除法的结果只会向下取整数。

>>> num = 5
>>> num.__truediv__(3)
1.6666666666666667
>>> num.__floordiv__(3)
1 # 向下取整
>>> num = 7
>>> num.__floordiv__(3)
2

在运算中,普通除法使用/表示,而地板除使用//表示。

>>> 5 / 3
1.6666666666666667
>>> 5 // 3
1

任务

Rational类虽然可以做加法,但无法做减法、乘法和除法,请继续完善Rational类,实现四则运算。

参考答案
def gcd(a, b):
    if b == 0:
        return a
    return gcd(b, a % b)

class Rational(object):
    def __init__(self, p, q):
        self.p = p
        self.q = q
    def __add__(self, r):
        return Rational(self.p * r.q + self.q * r.p, self.q * r.q)
    def __sub__(self, r):
        return Rational(self.p * r.q - self.q * r.p, self.q * r.q)
    def __mul__(self, r):
        return Rational(self.p * r.p, self.q * r.q)
    def __truediv__(self, r):
        return Rational(self.p * r.q, self.q * r.p)
    def __str__(self):
        g = gcd(self.p, self.q)
        return '{}/{}'.format(int(self.p/g), int(self.q/g))

r1 = Rational(1, 2)
r2 = Rational(1, 5)
print(r1 + r2)
print(r1 - r2)
print(r1 * r2)
print(r1 / r2)

五、类的__slots__方法

由于Python是动态语言,任何实例在运行期都可以动态地添加属性。比如:

class Student(object):
    def __init__(self, name, gender, score):
        self.name = name
        self.gender = gender
        self.score = score

此时,Student类有三个属性,name、gender、score,由于是动态语言,在运行时,可以随意添加属性。

student = Student('Bob', 'Male', 99)
student.age = 12 # ==> 动态添加年龄age属性

如果要限制添加的属性,例如,Student类只允许添加 name、gender和score 这3个属性,就可以利用Python的一个特殊的__slots__来实现。

class Student(object):
    __slots__ = ('name', 'gender', 'score')
    def __init__(self, name, gender, score):
        self.name = name
        self.gender = gender
        self.score = score

使用__slots__ = ('name', 'gender', 'score')限定Student类的属性,这个时候在外部再次添加动态属性age,将会报错。

student = Student('Bob', 'Male', 99)
>>> student.age = 12 # ==> 动态添加年龄age属性
Traceback (most recent call last):
AttributeError: 'Student' object has no attribute 'age'

__slots__的目的是限制当前类所能拥有的属性,避免因为外部属性的操作导致类属性越来越难以管理。

任务

假设Person类通过__slots__定义了name和gender,请在派生类Student中通过__slots__继续添加score的定义,使Student类可以实现name、gender和score 3个属性。

参考答案
class Person(object):

    __slots__ = ('name', 'gender')

    def __init__(self, name, gender):
        self.name = name
        self.gender = gender


class Student(Person):

    __slots__ = ('score',)

    def __init__(self, name, gender, score):
        self.name = name
        self.gender = gender
        self.score = score


s = Student('Bob', 'male', 59)
s.name = 'Tim'
s.score = 99
print(s.score)

六、类的__call__方法

在Python中,函数其实是一个对象,我们可以将一个函数赋值给一个变量,而不改变函数的功能。

>>> f = abs
>>> f
<built-in function abs>
>>> abs
<built-in function abs>
>>> f.__name__
'abs'
>>> f(-123)
123

把内建函数abs()赋值给变量f之后,可以看到f就和abs一样,都是。

由于 f 可以被调用,所以,f 被称为可调用对象,而事实上,所有的函数都是可调用对象。

如果把一个类实例也变成一个可调用对象,可以实现一个特殊的方法__call__()

例如,我们把Person类变成一个可调用对象:

class Person(object):
    def __init__(self, name, gender):
        self.name = name
        self.gender = gender

    def __call__(self, friend):
        print('My name is {}...'.format(self.name))
        print('My friend is {}...'.format(friend))

接着我们初始化一个Person对象,并对这个对象通过函数的方式调用:

>>> p = Person('Bob', 'Male')
>>> p('Alice') # ==> 用函数的方式调用Person类的实例p
My name is Bob...
My friend is Alice...

任务

请实现前面介绍过的斐波那契数列类Fib,加入__call__方法,使得调用的方式如下简单。

>>> f = Fib()
>>> print f(10)
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
参考答案
class Fib(object):
    def __init__(self):
        self.res = []

    def __call__(self, num):
        a = 0
        b = 1
        for x in range(num):
            self.res.append(a)
            a, b = b, a + b
        return self.res

f = Fib()
print(f(10))

Python进阶教程笔记(四)模块和包

一、定义模块

Python语言本身提供了非常多的模块,比如数学模块math、cmath、decimal、statistics;文件模块pathlib、stat、shutil等;除了使用官方模块,有时候也需要自定义模块。

如果我们需要创建一个tools模块,用来实现众多的工具函数,那么我们可以创建一个tools.py的文件,并在这个文件里面实现一些函数,如:say_hello()函数、say_goodbye()函数。

# tools.py
def say_hello():
    print('hello')

def say_goodbye():
    print('goodbye')

这样就定义了一个叫tools的模块,接着就可以使用这个模块了,在使用之前,我们需要先导入模块,下面我们来详细学习。

任务

定义一个公共模块common.py,在common.py中,包含公共函数say_hello(name),它接受一个参数,输出:Hello 的结果。

参考答案
# common.py

def say_hello(name):
    print('Hello {}'.format(name))

二、导入模块

要使用一个模块,我们必须首先导入该模块。Python使用import语句导入一个模块,Python官方提供很多有用的模块,比如:os模块、sys模块、time模块、math模块等等。

导入官方模块,不需要考虑路径的问题,例如,导入系统自带的模块 math,直接导入即可。如果是导入自定义模块,则需要考虑路径问题,我们下节课继续学习。

导入官方模块math:

import math

导入以后,你就可以认为math是一个指向已导入模块的变量,通过该变量,我们可以访问math模块中所定义的所有公开的函数、变量和类:

# 属性:圆周率
>>> import math
>>> math.pi
3.141592653589793

# 函数:次方
>>> math.pow(2, 3)
8.0

如果希望导入模块的指定部分属性或函数,那么使用from...import...语句。

>>> from math import pi
>>> print(pi)
3.141592653589793

这个时候,由于pow()函数没有导入,所以是不能使用pow()函数的。

如果希望导入模块里面的所有内容,那么使用from ...import *语句。

>>> from math import *
>>> print(pi)
3.141592653589793
>>> pow(2, 3)
8.0

如果从一个模块导入函数,有可能会遇到导入的函数与本文件的函数冲突的情况。例如:本文件定义了一个pow()函数,同时从math模块也导入了一个pow()函数,这种情况下就会引起冲突;事实上,这种冲突的情况经常发生。

有两种方法可以解决这个问题,第一种是直接导入模块,不指定导入模块里面的具体内容;第二种方法就是使用from ... import as ...语句,as类似重命名,可以把导入的函数或属性重命名为别的名字。

>>> from math import pow as mathpow
>>> mathpow(2, 3)
8.0

任务

math模块还提供了非常多的数学计算函数,比如:正弦sin()函数,余弦cos()函数,请使用两种导入的方式,使用这两个函数。

参考答案
import math
math.sin(0)
math.cos(0)

# 或者
from math import sin, cos
sin(0)
cos(0)

三、模块导入的路径

导入官方模块的时候,不需要考虑路径问题,这是因为在搜索模块的时候,会默认包含官方模块的路径,所以导入官方模块不需要考虑路径的问题。

如果需要导入自定义模块,则需要了解Python导入模块搜索的路径。

通过sys模块,可以知道导入模块的路径。

>>> import sys
>>> sys.path
['', '/data/miniconda3/lib/python3.8', '/data/miniconda3/lib/python3.8/site-packages']

它返回的是一个列表,表示的是在搜索Python模块时,会搜索的路径,在示例中,返回了四个路径。我们分析一些关键路径:

第一个路径是'',它是一个空字符串,表达的是当前路径的意思。

第二个路径是/data/miniconda3/lib/python3.8,它是Python默认模块的存放的路径,在这个路径下,可以发现有os、sys等模块的代码。

第三个路径是/data/miniconda3/lib/python3.8/site-packages,它是第三方模块代码的存放路径,在这个路径下,存放的是需要安装的第三方模块。

那如何使用我们前面定义的tools.py模块呢? 我们在tools.py同级目录,创建main.py文件:

# main.py

import tools # 导入模块

tools.say_hello() # 调用模块里面的say_hello()函数
tools.say_goodbye() # 调用模块里面的say_goodbye()函数

就可以运行了。 因为在搜索包的路径时,会搜索当前路径(上述:sys.path结果的第一项),因此在同一个目录内的tools.py模块,可以被搜索到,所以能够import进来。

四、安装第三方模块的方法

尽管Python的官方模块已经提供了非常强大的能力,但是仍有大量热心开发者提供了非常好用的第三方库,在实际开发中,也会经常使用,比如Web开发框架,Django、Flask,异步任务框架:Celery等。 在安装Python的时候,Python环境提供了安装第三方模块的工具:pip,通过这个工具,可以非常快捷的安装第三方模块。 安装Django模块:

pip install django

卸载Django模块:

pip uninstall django

Python进阶教程笔记(五)读写文件

一、向Python程序输入内容

到目前为止,我们编写的程序都是直接运行的,在运行过程中并没有接收程序外部的输入。比如,通过Python程序,我们可以快速算出从1到100的乘法结果。

result = 1
for i in range(1, 101):
    result = result * i

print(result)

但是如果需要计算从1到200的乘法结果,则只能通过修改程序去实现。

result = 1
for i in range(1, 201):
    result = result * i

print(result)

如果可以通过输入,改变计算的范围,那就好了,input()函数可以接收外部的输入。

>>> num = input('please input number: ')
please input number: 201
>>> print(num) 
201

因此,通过input()函数,则可以改变上面程序运行的范围,注意:输入的是字符串,需要转型为数字类型。

num = input('please input number: ')

num = int(num)
result = 1
for i in range(1, num):
    result = result * i

print(result)

任务

eval()函数可以把字符串转换为等值的结果,比如eval(‘1+1’),得到结果为2。请使用eval实现一个简单的计算器,可以输入表达式并打印计算结果。

参考答案
import math
math.sin(0)
math.cos(0)

# 或者
from math import sin, cos
sin(0)
cos(0)

二、打开文本文件

通过print()可以从数据输出数据,通过input()可以向程序输入数据,但这些都是标准屏幕上的操作,本节课学习文件的读写操作。

Python 提供了open()函数,可以打开一个文件,得到一个文件file对象,而file对象提供相关的方法对文件内容进行读写等操作。

open()函数有若干个参数,比较重要的是以下三个参数:

  1. 文件路径:指定需要打开的文件的文件路径
  2. 打开模式:针对不同文件(二进制文件、文本文件)以及不同操作(读操作、写操作),会有不同的打开模式
  3. 编码:设定打开文件的默认编码

常用的打开模式如下:

模式 描述
t 文本模式(默认)
x 写模式,新建一个文件
b 二进制模式,打开二进制文件
+ 更新一个文件(可读可写)
r 以只读模式打开一个文件
rb 以二进制格式只读模式打开一个文件
w 打开一个文件进行写入,如果文件内容已存在,会清除原有的内容
wb 以二进制格式只写模式打开一个文件,会清除原有的内容
a 打开一个文件并追加内容,会往文件尾部添加内容
ab 以二进制格式打开一个文件并追加内容,会往文件尾部添加内容
w+ 打开一个文件进行读写,如果文件内容已存在,会清除原有的内容
a+ 打开一个文件并使用追加进行读写

注意,为了安全操作文件,文件使用完毕后,需要使用close()函数正确关闭。 在当前目录下新建一个test.txt文件,并新建一个main.py,此时文件目录如下:

|-- test.txt
+-- main.py
f = open('test.txt', 'r') # 打开test.txt文件
type(f) # 打印f的类型(<class '_io.TextIOWrapper'>)
f.close() # 关闭文件

注意,在打开文本文件是并不需要特别指定模式t,因为默认就是以文本方式打开文件的。

三、打开二进制文件

除了文本以外,还有大量的非文本文件,比如图片、压缩文件、视频文件、音乐文件等等,这种文件统称为二进制文件,在Python中打开二进制文件,需要不同的打开模式。

模式 描述
b 二进制模式,打开二进制文件
wb 以二进制格式只写模式打开一个文件,会清除原有的内容
ab 以二进制格式打开一个文件并追加内容,会往文件尾部添加内容
rb 以二进制格式只读模式打开一个文件
f = open('test.jpg', 'rb')
f.close()

四、读取文件内容

打开文件之后,就可以读取文件的内容,文件对象提供多种读取文件内容的方法。 打开test.txt文件:

f = open('test.txt', 'r') # 打开test.txt文件
f.close() # 关闭文件

test.txt文件有以下内容

Hello World.
Hello Python.
Hello Imooc.

读取若干字符

文件对象提供read()方法,可以读取文件中的若干个字符,它提供一个参数size,可以指定读取字符的数量。

s = f.read(5)
print(s) # ==> Hello

当read()之后,访问文件的游标就会移动到第六个字符前面,此时,继续read,将得到Hello后面的结果。

s = f.read(6)
print(s) # ==> ' World'

读取一行

文件对象提供readline()方法,和read()方法类似,可以读取文件中的若干个字符,它也提供一个参数size,可以指定读取字符的数量,不过和read()方法不同的是,readline()方法遇到一行结束的时候,就会返回。

f.close()
f = open('test.txt', 'r') # 重新打开文件
s = f.readline(20)
print(s)  # ==> 'Hello World.\n'

可以看到,打印的内容并没有20个字符,readline最多返回一行的所有字符。

读取多行

文件对象提供readlines()方法,可以读取多行字符,返回一个列表。它提供一个hint参数,表示指定读取的行数,没有指定则默认以列表的形式返回文件所有的字符串。

f.close()
f.open('test.txt', 'r')
s = f.readlines()
print(s) # ==> ['Hello World.\n', 'Hello Python.\n', 'Hello Imooc.\n']

五、把字符串写入文件

要把字符串内容写入文件,需要使用w的模式打开文件。

模式 描述
w 打开一个文件进行写入,如果文件内容已存在,会清除原有的内容
wb 以二进制格式只写模式打开一个文件,会清除原有的内容
w+ 打开一个文件进行读写,如果文件内容已存在,会清除原有的内容
f = open('test.txt', 'w')

写入若干字符

文件对象提供write方法向文件内写入若干字符,它接受一个字符串参数,表示需要写入的字符串。

f = open('test.txt', 'w')
f.write('Hello World\n')
f.close()

写入若干行

文件对象提供writelines()方法向文件内容写入多行数据,它接受一个列表,表示需要写入的字符串列表。

lines = ['Hello World\n', 'Hello Python\n', 'Hello Imooc\n']
f = open('test.txt', 'w')

f.writelines(lines)
f.close()

任务

有test.txt文件,包含以下内容:

Hello World
Hello Python
Hello Imooc

请从test.txt文件读取以上内容,并将每一行字符串反转,写入test1.txt文件。

dlroW olleH
nohtyP olleH
coomI olleH
参考答案
  1. 字符串反转可以使用切片实现: reverse = str_[::-1]

  2. 换行符是’\n’,字符串反转的时候,换行符也会翻转

f = open('test.txt', 'r')
lines = f.readlines()
f1 = open('test1.txt', 'w')
for line in lines:
    line = line[::-1]
    f1.write(line)

f1.close()
f.close()

六、往文件追加内容

通过w的打开方式打开文件,会清空文件的内容,这在很多场景下是不合适的,比如写系统日志的时候,需要累积随时间推移的所有数据。

Python提供文件追加内容的打开模式,可以往文件尾部添加内容,又不清空文件原有的内容。

模式 描述
a 打开一个文件并追加内容,会往文件尾部添加内容
ab 以二进制格式打开一个文件并追加内容,会往文件尾部添加内容
a+ 打开一个文件并使用追加进行读写
f = open('test.txt', 'a')
f.write('Hello Everyone\n')
f.close()

使用a的打开方式打开文件,文件游标默认是在文件的尾部,因此,可以便捷的往文件尾部添加内容,除此以外,文件对象还提供seek()方法,可以移动文件的游标位置,它接受一个参数,表示文件的位置,0:文件首部,1:当前位置,2:文件尾部,通过seek()可以把文件游标移动到文件首部但不删除文件的内容。

f = open('test.txt', 'a+')
content = f.readlines()
print(content) # ==> []
f.seek(0)
content = f.readlines()
print(content) # ==> ['Hello World\n', 'Hello Python\n', 'Hello Imooc\n']

第一次print(content)的时候,由于文件游标在文件的尾部,所以readlines()读取不到任何数据,打印了空的结果,第二次print(content)的时候,由于通过seek(0),文件游标移动到了文件的首部,因此readlines()就返回了文件所有的内容。

七、正确关闭文件

在进行文件操作的时候,正确关闭一个文件非常重要,如果在文件读写后,没有正确关闭一个文件的话,则有可能导致文件损坏,文件内容丢失等问题。

在一般情况下,我们使用文件对象的close()方法,来关闭一个文件。 但是,使用close()方法,也不是100%安全的,如果在close()文件之前,程序异常退出了,那么文件也得不到正确的关闭。比如:

f = open('test.txt', 'a+')
exit(-1) # ==> 模拟程序异常退出
f.close() # ==> close语句永远的不到执行

在实际工程中,close()文件之前,为了正确关闭文件,需要考虑各种异常情况,这是非常麻烦的一件事,Python提供with关键字,可以免除这类后顾之忧。

with关键字对资源进行访问的场合,会确保不管在使用过程中是否发生异常,都会执行必要的“清理”的操作,释放资源,比如文件使用后自动关闭等等。

with的使用方法如下:

with open('test.txt', 'r') as f:
    content = f.readlines()
    for line in content:
        print(line)

Python进阶教程笔记(六)网络编程

2020-10-302020-10-30 11:43:20阅读 1450

一、套接字Socket与套接字编程

要进行网络通信,需要建立起通信双方的连接,连接的双方分别称为客户端和服务端,在Python中,使用套接字socket来建立起网络连接。 套接字包含在socket模块中:

import socket
socket.socket()

对于客户端和服务端,都是使用socket来建立连接的,但是在使用行为上,客户端和服务端会有一些不一样。

服务端建立需要四个步骤:新建socket、绑定IP和端口(bind)、监听连接(listen)、接受连接(accept)。

客户端建立则简单一些,仅需两个步骤:新建socket、连接服务端(connect)。 当网络连接上以后,客户端和服务端就可以进行数据通信了,套接字通过send()函数发送数据,通过recv()函数接收数据。

先看服务端的过程,新建一个server.py的文件:

import socket

server = socket.socket() # 1. 新建socket
server.bind(('127.0.0.1', 8999)) # 2. 绑定IP和端口(其中127.0.0.1为本机回环IP)
server.listen(5) # 3. 监听连接
s, addr = server.accept() # 4. 接受连接
print('connect addr:{}'.format(addr))
content =s.recv(1024)
print(str(content, encoding='utf-8'))  # 接受来自客户端的消息,并编码打印出来
s.close()

如上,服务端就编写完毕,接下来是编写客户端,新建一个client.py的文件:

import socket

client = socket.socket() # 1. 新建socket
client.connect(('127.0.0.1', 8999)) # 2. 连接服务端(注意,IP和端口要和服务端一致)
client.send(bytes('Hello World. Hello Socket', encoding='utf-8')) # 发送内容,注意发送的是字节字符串。
client.close()

接着在一个终端先运行服务端:

python server.py

然后再在另外一个终端运行客户端:

python client.py

在服务端的终端,将会输出以下信息:

connect addr:('127.0.0.1', 50382)
b'Hello World. Hello Socket'

二、自带的HTTP服务器

在互联网的世界中,网页、手机H5等都是通过HTTP向用户提供服务的,这些信息存储在HTTP服务器中,HTTP服务器是一种特殊的Socket服务器,它在网络连接之上,定义了HTTP协议,使得网页、手机H5等数据,都可以以标准的HTTP协议进行传输。

Python提供了简易的HTTP服务器,可以直接运行起来。 在终端,输入这条命令,就可以启动一个HTTP服务器。

python -m http.server

启动成功后,会输出以下信息:

Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...

提示HTTP服务器在本机8000端口运行,接着就可以在浏览器输入http://127.0.0.1:8000看到由这个服务器提供的网页。

这个HTTP服务器会把运行目录的所有文件列出来,并提供下载功能。

三、发送HTTP请求

在上一节,使用了Python自带的功能启动了一个HTTP服务器,并通过浏览器浏览到了这个HTTP服务器提供的页面。在浏览的过程中,实际上是浏览器向HTTP服务器发送了一个HTTP请求。

除了使用浏览器发送HTTP请求,通过代码也可以向HTTP服务器发送请求,Python提供了相关的库urllib,通过urllib包里面的request,可以向其他HTTP服务器发起请求。

from urllib import request
response = request.urlopen('https://www.imooc.com') # 向慕课网官网发出请求
print(response) # ==> <http.client.HTTPResponse object at 0x000001377D631948>

请求成功的话,会得到一个HTTPResponse,它是来自HTTP服务器的一个回应,可以把这个回应的一些信息打印出来。

状态码:

print(response.status) # ==> 200

状态码是一个三位整数,在HTTP协议的标准里面,定义了很多状态码,其中200表示请求是正常的。

响应头:

for k, v in response.getheaders():
    print('{}: {}'.format(k, v))

将会输出以下信息,这是HTTPResponse附带的一些信息,包括服务端的服务器是什么、请求时间、内容类型、内容长度等等。

Server: openresty
Date: Thu, 20 Aug 2020 08:16:07 GMT
Content-Type: text/html; charset=UTF-8
Content-Length: 376639
Connection: close
Vary: Accept-Encoding
Vary: Accept-Encoding
X-Varnish: 280516221 281284036
Age: 29
Via: 1.1 varnish (Varnish/6.0)
X-Cache: HIT from CS42
Accept-Ranges: bytes

四、requests库

Python官方提供的urllib库可以满足一般情况下的HTTP操作,但是urllib这个库设计是用来处理url地址的,并不是专门处理HTTP操作的包。因此,在很多场景下,一般会使用requests库来进行HTTP请求。

requests库是著名的Python第三方库,使用requests库,可以定制化你的HTTP请求,包括请求方法,请求参数等等。

由于requests是第三方库,因此在使用前,需要安装。

pip install requests

安装完成后,使用requests库来请求百度。

response = requests.get('https://www.baidu.com')
# 打印状态码
print(response.status_code)
# 打印回应头
print(response.headers)

在一般的使用上,requests和urllib没有太大区别,但是在复杂的场景中,requests可以提供urllib无法提供的强大功能。因此,在使用上,建议使用requests库代替urllib库来进行HTTP请求等的操作。

五、HTTP响应的内容

通过urllib或者requests请求后,会得到一个HTTPResponse,HTTPResponse拥有状态码、回应头等的信息。

但我们知道,对于一个页面,通常是由文字、图片等信息组成的,这些属于一个HTTPResponse的内容。

import requests

response = requests.get('https://www.baidu.com')
content = str(response.content, encoding='utf-8') # ==> 打印具体内容

打印的结果是一个很长的字符串,显得杂乱无章,但其实它是由结构的,它是一个标准的HTML页面,可以从页面内容里面获取很多有用的数据。

网络爬虫是典型的应用程序,它的工作原理就是通过不断的请求互联网的页面,并从回应中解析获取出有用的数据;数据积累后,可以有很多用处。

通过requests获得网页的内容后,我们可以尝试使用一些简单的方法获取页面的内容。

content_list = content.split('\n') # 分行
len(content_list) # 打印页面内容的行数

在网页中,页面内部链接其他资源的信息是通过href提供的,通过字符串匹配的方式可以过滤出包含链接的行。

for line in content_list:
    if 'href' in line:
        print(line.strip())

Python进阶教程笔记(七)函数式编程

2020-10-302020-10-30 11:43:38阅读 1880

一、什么是函数式编程

  • 函数:function,逻辑功能的单位
  • 函数式:functional,一种编程范式
  • 特点:
  1. 把计算视为函数而不是指令
  2. 代码简洁,方便阅读,容易理解
  3. 纯函数式编程:不需要变量、测试简单

二、Python的函数式编程

  • 不是纯函数式编程:允许有变量
  • 支持高阶函数:函数可以作为变量
  • 支持闭包:可以返回函数
  • 支持匿名函数

三、把函数作为参数

在前面,我们了解了高阶函数的概念,并编写了一个简单的高阶函数:

def add(x, y, f):
    return f(x) + f(y)

如果传入abs作为参数f的值:

add(-5, 9, abs)

根据函数的定义,函数执行的代码实际上是:

abs(-5) + abs(9)

由于参数 x, y 和 f 都可以任意传入,如果 f 传入其他函数,就可以得到不同的返回值。

四、map()函数

map()是 Python 内置的高阶函数,它接收一个函数 f 和一个 list,并通过把函数 f依次作用在list的每个元素上,map()函数会返回一个迭代器,可以依次迭代得到原来list的元素被函数f处理后的结果。

>>> map(f, list)

例如,对于list [1, 2, 3, 4, 5, 6, 7, 8, 9]。 如果希望把list的每个元素都作平方,就可以利用map()函数。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PMeeM1I2-1651453818303)(https://ask.qcloudimg.com/http-save/yehe-1217896/vnh16hhl2j.png?imageView2/2/w/1620)]

我们定义需要传入函数f(x)=x*x,就可以利用map()函数完成这个计算:

def f(x):
    return x*x

for item in map(f, [1, 2, 3, 4, 5, 6, 7, 8, 9]):
    print(item)

得到结果:

[1, 4, 9, 10, 25, 36, 49, 64, 81]

由于list包含的元素可以是任何类型,因此,map() 不仅仅可以处理只包含数值的 list,事实上它可以处理包含任意类型的 list,只要传入的函数f可以处理这种数据类型。

五、reduce()函数

和map函数一样,reduce()函数也是Python内置的一个高阶函数。reduce()函数接收的参数和 map() 类似,一个函数 f,一个list,但行为和 map()不同,reduce()传入的函数 f 必须接收两个参数,reduce()对list的每个元素反复调用函数f,并返回最终结果值。

在python2中,reduce()函数和map()函数一样,可以直接使用,但是在python3中,reduce()函数被收录到functools包内,需要引入functools才可以使用。 例如,编写一个f函数,接收x和y,返回x和y的和:

def f(x, y):
    return x + y

调用 reduce(f, [1, 3, 5, 7, 9]):

from functools import reduce

def f(x, y):
    return x + y

print(reduce(f, [1,3,5,7,9])) # ==> 25

得到的结果是25,实际过程是这样的,reduce()函数会做如下计算:

先计算头两个元素:f(1, 3),结果为4;
再把结果和第3个元素计算:f(4, 5),结果为9;
再把结果和第4个元素计算:f(9, 7),结果为16;
再把结果和第5个元素计算:f(16, 9),结果为25;
由于没有更多的元素了,计算结束,返回结果25

上述计算实际上是对 list 的所有元素求和。虽然Python内置了求和函数sum(),但是,利用reduce()求和也很简单。 reduce()还可以接收第3个可选参数,作为计算的初始值。如果把初始值设为100,计算:

print(reduce(f, [1, 3, 5, 7, 9], 100)) # ==> 125

结果将变为125,因为第一轮计算是:

计算初始值和第一个元素:f(100, 1),结果为101。

六、filter()函数

filter()函数是 Python 内置的另一个有用的高阶函数,filter()函数接收一个函数 f 和一个list,这个函数 f 的作用是对每个元素进行判断,返回 True或 False,filter()根据判断结果自动过滤掉不符合条件的元素,并返回一个迭代器,可以迭代出所有符合条件的元素。

例如,要从一个list [1, 4, 6, 7, 9, 12, 17]中删除偶数,保留奇数,首先,要编写一个判断奇数的函数:

def is_odd(x):
    return x % 2 == 1

然后,利用filter()过滤掉偶数:

for item in filter(is_odd, [1, 4, 6, 7, 9, 12, 17]):
    print(item)

结果:1,7,9,17。 利用filter()函数,可以完成很多很有用的功能,例如,删除 None 或者空字符串:

def is_not_empty(s):
    return s and len(s.strip()) > 0

for item in filter(is_not_empty, ['test', None, '', 'str', '  ', 'END']):
    print(item)

结果:test, str, END 注意:注意: s.strip()会默认删除空白字符(包括’\n’, ‘\r’, ‘\t’, ’ '),如下:

s = '     123'
s.strip() # ==> 123
s= '\t\t123\r\n'
s.strip() # ==> 123

七、自定义排序函数

Python内置的 sorted()函数可对list进行排序:

>>> sorted([36, 5, 12, 9, 21])
[5, 9, 12, 21, 36]

可以看到,sorted()函数,默认是由小到大排序列表的元素。

>>> score = [('Alice', 72), ('Candy', 90), ('Bob', 62)]
>>> sorted(score)
[('Alice', 72), ('Bob', 62), ('Candy', 90)]

当list的每一个元素又是一个容器时,则会以第一个元素来排序,比如在score中,每个元素都是包含名字和成绩的一个tuple,sorted()函数则按名字首字母进行了排序并返回。

对于上述排序成绩的情况,默认是按照第一个名字进行排序的,有没有办法让sorted()函数按照成绩来进行排序呢?

如果需要按照成绩高低进行排序,需要指定排序的字段是成绩,sorted接受key参数,用来指定排序的字段,key的值是一个函数,接受待排序列表的元素作为参数,并返回对应需要排序的字段。因此,sorted()函数也是高阶函数。

def k(item):
    return item[1] # ==> 按成绩排序,成绩是第二个字段

sorted(score, key=k)

得到结果:[(‘Bob’, 62), (‘Alice’, 72), (‘Candy’, 90)] 。 如果需要倒序,指定reverse参数即可。

sorted(score, key=k, reverse=True)

得到结果:[(‘Candy’, 90), (‘Alice’, 72), (‘Bob’, 62)] 。

八、返回函数

在函数内部,是可以定义子函数的。

def func():
    # 定义子函数
    def sub_func():
        print('call sub_func.')
    sub_func()

>>> func()
call sub_func.

作为高阶函数,可以接受函数作为参数,其实高阶函数,除了不仅仅可以返回int、str、list、dict等数据类型,还可以返回函数。因此,可以把函数的子函数返回。

def f():
    print('call f()...')
    # 定义函数g:
    def g():
        print('call g()...')
    # 返回函数g:
    return g

仔细观察上面的函数定义,我们在函数 f 内部又定义了一个函数 g。由于函数 g 也是一个对象,函数名 g 就是指向函数 g 的变量,所以,最外层函数 f 可以返回变量 g,也就是函数 g 本身。 调用函数 f,我们会得到 f 返回的一个函数:

>>> x = f()   # 调用f()
call f()...
>>> x   # 变量x是f()返回的函数:
<function f.<locals>.g at 0x7f4a4936dbf8>
>>> x()   # x指向函数,因此可以调用
call g()...   # 调用x()就是执行g()函数定义的代码

有必要注意的是,返回函数和返回函数值的语句是非常类似的,返回函数时,不能带小括号,而返回函数值时,则需要带上小括号以调用函数。

# 返回函数
def myabs():
    return abs

# 返回函数值
def myabs(x):
    return abs(x)

返回函数有很多应用,比如可以将一些计算延迟执行,举个例子,定义一个普通的求和函数。

def calc_sum(list_):
    return sum(list_)

调用calc_sum()函数时,将立刻计算并得到结果:

>>> calc_sum([1, 2, 3, 4])
10

但是,如果返回一个函数,就可以“延迟计算”:

def calc_sum(list_):
    def lazy_sum():
        return sum(list_)
    return lazy_sum

调用calc_sum()并没有计算出结果,而是返回函数:

>>> f = calc_sum([1, 2, 3, 4])
>>> f
<function calc_sum.<locals>.lazy_sum at 0x7f4a4936db70>

对返回的函数进行调用时,才计算出结果:

>>> f()
10

由于可以返回函数,我们在后续代码里就可以决定到底要不要调用该函数。

九、闭包

在函数内部定义的函数和外部定义的函数是一样的,只是他们无法被外部访问:

def g():
    print('g()...')

def f():
    print('f()...')
    return g

将g的定义移入函数 f 内部,防止其他代码调用 g:

def f():
    print('f()...')
    def g():
        print('g()...')
    return g

但是,考察上一小节定义的 calc_sum 函数:

def calc_sum(list_):
    def lazy_sum():
        return sum(list_)
    return lazy_sum

注意: 发现没法把 lazy_sum 移到 calc_sum 的外部,因为它引用了 calc_sum 的参数 list_。

像这种内层函数引用了外层函数的变量(参数也算变量),然后返回内层函数的情况,称为闭包(Closure)。

闭包的特点是返回的函数还引用了外层函数的局部变量,所以,要正确使用闭包,就要确保引用的局部变量在函数返回后不能变。举例如下:

# 希望一次返回3个函数,分别计算1x1,2x2,3x3:
def count():
    fs = []
    for i in range(1, 4):
        def f():
             return i*i
        fs.append(f)
    return fs

f1, f2, f3 = count()

你可能认为调用f1(),f2()和f3()结果应该是1,4,9,但实际结果全部都是 9(请自己动手验证)。

原因就是当count()函数返回了3个函数时,这3个函数所引用的变量 i 的值已经变成了3。由于f1、f2、f3并没有被调用,所以,此时他们并未计算 i*i,当 f1 被调用时:

>>> f1()
9     # 因为f1现在才计算i*i,但现在i的值已经变为3

因此,返回函数不要引用任何循环变量,或者后续会发生变化的变量。

十、匿名函数

高阶函数可以接收函数做参数,有些时候,我们不需要显式地定义函数,直接传入匿名函数更方便。

在Python中,对匿名函数提供了有限支持。还是以map()函数为例,计算 f(x)=x * x时,f(x)就是作为参数传入map的。 在前面,我们是显式的定义了一个f(x)的函数,除此以外,其实可以直接传入匿名函数。

匿名函数使用lambda定义:lambda x: x * x,就可以完成原来显式定义的f(x)函数的功能,冒号前面的x表示匿名函数的参数,后面的是一个表达式,匿名函数有个限制,就是只能有一个表达式,不写return,返回值就是该表达式的结果。

result = [item for item in map(lambda x: x * x, [1, 2, 3, 4, 5, 6, 7, 8, 9])]
print(result) # ==> [1, 4, 9, 16, 25, 36, 49, 64, 81]

同理,对于reduce()函数,也同样可以通过定义匿名函数来实现相同的逻辑。

from functools import reduce

reduce(lambda x, y: x + y, [1,3,5,7,9])

十一、编写无参数的decorator

Python的 decorator 本质上就是一个高阶函数,它接收一个函数作为参数,然后,返回一个新函数。 使用 decorator 用Python提供的 @ 语法,这样可以避免手动编写 f = decorate(f) 这样的代码。 考察一个@log的定义:

def log(f):
    def fn(x):
        print('call ' + f.__name__ + '()...')
        return f(x)
    return fn

对于阶乘函数,@log工作得很好:

@log
def factorial(n):
    return reduce(lambda x,y: x*y, range(1, n+1))

print(factorial(10))

结果:

call factorial()...
3628800

但是,对于参数不是一个的函数,调用将报错:

@log
def add(x, y):
    return x + y
print(add(1, 2))
>>> print(add(1, 2))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: fn() takes 1 positional argument but 2 were given

因为 add() 函数需要传入两个参数,但是 @log 写死了只含一个参数的返回函数。

要让 @log 自适应任何参数定义的函数,可以利用Python的 args 和 *kwargs,保证任意个数的参数总是能正常调用:

def log(f):
    def fn(*args, **kwargs):
        print('call ' + f.__name__ + '()...')
        return f(*args, **kwargs)
    return fn

十二、编写有参数的decorator

考察上一节的 @log 装饰器:

def log(f):
    def fn(x):
        print('call ' + f.__name__ + '()...')
        return f(x)
    return fn

发现对于被装饰的函数,log打印的语句是不能变的(除了函数名)。

如果有的函数非常重要,希望打印出’[INFO] call xxx()…‘。 有的函数不太重要,希望打印出’[DEBUG] call xxx()…'。 这时,log函数本身就需要传入’INFO’或’DEBUG’这样的参数,类似这样:

@log('DEBUG')
def my_func():
    pass

把上面的定义翻译成高阶函数的调用,就是:

my_func = log('DEBUG')(my_func)

上面的语句看上去还是比较绕,再展开一下:

log_decorator = log('DEBUG')
my_func = log_decorator(my_func)

上面的语句又相当于:

log_decorator = log('DEBUG')
@log_decorator
def my_func():
    pass

所以,带参数的log函数首先返回一个decorator函数,再让这个decorator函数接收my_func并返回新函数,相当于是在原有的二层嵌套里面,增加了一层嵌套:

def log(prefix):
    def log_decorator(f):
        def wrapper(*args, **kw):
            print('[{}] {}()...'.format(prefix, f.__name__))
            return f(*args, **kw)
        return wrapper
    return log_decorator

@log('DEBUG')
def test():
    pass
test()

执行结果:

[DEBUG] test()...

对于这种三层嵌套的decorator定义,你可以先把它拆开:

# 标准decorator:
def log_decorator(f):
    def wrapper(*args, **kw):
        print('[{}] {}()...'.format(prefix, f.__name__))
        return f(*args, **kw)
    return wrapper
return log_decorator

# 返回decorator:
def log(prefix):
    return log_decorator(f)

十三、偏函数

当一个函数有很多参数时,调用者就需要提供多个参数。如果减少参数个数,就可以简化调用者的负担。

比如,int()函数可以把字符串转换为整数,当仅传入字符串时,int()函数默认按十进制转换:

>>> int('12345')
12345

但int()函数还提供额外的base参数,默认值为10。如果传入base参数,就可以做 N 进制的转换:

>>> int('12345', base=8)
5349
>>> int('12345', 16)
74565

假设要转换大量的二进制字符串,每次都传入int(x, base=2)非常麻烦,于是,我们想到,可以定义一个int2()的函数,默认把base=2传进去:

def int2(x, base=2):
    return int(x, base)

这样,我们转换二进制就非常方便了:

>>> int2('1000000')
64
>>> int2('1010101')
85

偏函数指的就是“创建一个调用另外一个部分参数或变量已经预置的函数”的函数的用法,如上所示,int()函数的base参数,没有指定的时候,默认是以十进制工作的,当指定base=2的时候,int2实际上就变成了部分参数(base)已经预置了的偏函数。 functools.partial就是帮助我们创建一个偏函数的,不需要我们自己定义int2(),可以直接使用下面的代码创建一个新的函数int2:

>>> import functools
>>> int2 = functools.partial(int, base=2)
>>> int2('1000000')
64
>>> int2('1010101')
85

猜你喜欢

转载自blog.csdn.net/m0_49960764/article/details/124536662