回炉-Python基础教程-2

  |  

摘要: 《Python基础教程》笔记 part2

【对算法,数学,计算机感兴趣的同学,欢迎关注我哈,阅读更多原创文章】
我的网站:潮汐朝夕的生活实验室
我的公众号:算法题刷刷
我的知乎:潮汐朝夕
我的github:FennelDumplings
我的leetcode:FennelDumplings


写在前面

【回炉-Python基础教程】系列连载主要回炉我之前看过的一本比较入门但是很系统的 Python 的书。

本书信息:

本书包括Python程序设计的方方面面,主要分为 5 个部分

  • Python基础知识和基本概念,包括列表、元组、字符串、字典以及各种语句
  • Python相对高级的主题,包括抽象、异常、魔法方法、特性、迭代器、文件处理
  • Python与数据库、网络、C语言等工具结合使用,发挥出Python的强大功能
  • Python程序测试、打包、发布等知识
  • 10个具有实际意义的Python项目的开发过程

往期回顾


本文是《Python基础教程》回炉系列的第二部分中的抽象这个话题中的一些容易忘的点。涉及书中章节 Chap6 ~ Chap7,各章节记录的要点如下:

  • Chap 6 Abastaction
    • Function
    • Parameter
      • Modify My Parameters
      • Keyword Parameters and Defaults
      • Collecting Parameters
      • Reversing the Process
      • Parameter Practice
    • Scoping
      • NESTED SCOPES
    • Recursion
      • Two Classics: Factorial and Power
      • Binary Search
    • functional programming
  • Chap 7 More Abstraction
    • Objects
      • Polymorphism
      • Encapsulation
      • Inheritance
    • Classes
      • Attributes, Functions, and Methods
      • Privacy Revisited
      • The Class Namespace
      • Investigating Inheritance
      • Multiple Superclasses
      • Interfaces and Introspection
      • Abstract Base Classes
    • Some Thoughts on Object-Oriented Design

6. Abastaction

Function

  • 内建函数 callable 判定变量是否为可调用对象

  • docstring, 通过 __doc__ 可以查看

1
2
3
4
5
def square(x):
'Calculates the square of the number x.'
return x * x

square.__doc__
  • all functions do return something; it’s just that they return None when you don’t tell them what to return

  • If you return values from inside if statements and the like, be sure you’ve covered every case. For example you don’t accidentally return None when the caller is expecting a sequence.

Parameter

确认两件事

  1. the function does its job if it is supplied with acceptable parameters
  2. the function preferably fails in an obvious manner if the parameters are wrong.(You do this with assert or exceptions in general. You’ll learn more about exceptions in Chapter 8.)
  • Assigning a new value to a parameter inside a function won’t change the outside world at all.
  • parameters are kept in what is called a local scope.
  • Strings (and numbers and tuples) are immutable, which means that you can’t modify them (that is, you can only replace them with new values).
  • 对于 mutable data structure (例如 list) 需要注意: When two variables refer to the same list, they refer to the same list. It’s really as simple as that. If you want to avoid this, you must make a copy of the list.
  • When you do slicing on a sequence, the returned slice is always a copy. Thus, if you make a slice of the entire list, you get a copy.
1
2
3
4
names = ['czx', 'sjtu', 'bjhwzx']
n = names[:]
n is names # False
n == names # True

Using a function to change a data structure (such as a list or a dictionary) can be a good way of introducing abstraction into your program.

Modify My Parameters

In some languages (such as C++), rebinding parameters and having these changes affect variables outside the function is an everyday thing.

In Python, it’s not directly possible; you can modify only the parameter objects themselves. But what if you have an immutable parameter, such as a number? it can’t be done. What you should do is return all the values you need from your function

1
2
def inc(x): 
return x + 1

If you really wanted to modify your parameter, you could use a trick such as wrapping your value in a list, like this:

1
2
def inc(x): 
x[0] = x[0] + 1

Keyword Parameters and Defaults

  • positional parameters: their positions are important
  • keyword parameters
  • You can combine positional and keyword parameters. The only requirement is that all the positional parameters come first.

Unless you know what you’re doing, you might want to avoid mixing positional and keyword parameters. that approach is generally used when you have a small number of mandatory parameters and many modifying parameters with default values.

Collecting Parameters

1
2
def print_params(*params):
print(params)
  • The star in front of the parameter puts all the values into the same tuple(params 是元组).
  • In assignments, the starred variable collects superfluous values in a list rather than a tuple,
  • the star means “Gather up the rest of the positional parameters.” If you don’t give any parameters to gather, params will be an empty tuple.
1
2
3
def print_params_2(title, *params):
print(title)
print(params)

Just as with assignments, the starred parameter may occur in other positions than the last. Unlike with assignments, though, you have to do some extra work and specify the final parameters by name.

1
2
3
4
5
6
def in_the_middle(x, *y, z):
print(x, y, z)

in_the_middle(1, 2, 3, 4, 5, z=7) # specify the final parameters by name.
# 打印结果为 1 (2, 3, 4, 5) 7
# in_the_middle(1, 2, 3, 4, 5, 7) 会报错
  • The star doesn’t collect keyword arguments. 例如 print_params_2('Hmm...', something=42) 会报错
  • We can gather keyword arguments with the double star.
1
2
3
def print_params_3(**params): 
print(params)
# params 是字典

These various techniques work well together.

1
2
3
4
5
6
7
8
9
10
def print_params_4(x, y, z=3, *pospar, **keypar):
print(x, y, z)
print(pospar)
print(keypar)

print_params_4(1, 2)
# 打印结果为
# 1 2 3
# ()
# {}

Reversing the Process

Now you’ve learned about gathering up parameters in tuples and dictionaries, but it is in fact possible to do the “opposite” as well, with the same two operators, * and **.

Instead of gathering the parameters, we want to distribute them. This is simply done by using the * operator when calling the function rather than when defining it.

1
2
3
4
5
6
7
8
9
10
def add(x, y):
return x + y
params = (1, 2)
add(*params)

def hello_3(greeting='Hello', name='world'):
print('{}, {}!'.format(greeting, name))

params = {'name': 'Sir Robin', 'greeting': 'Well met'}
hello_3(**params)

Using * (or **) both when you define and when you call the function will simply pass the tuple or dictionary right through, so you might as well not have bothered.

1
2
3
4
5
6
7
8
9
10
11
def with_stars(**kwds):
print(kwds['name'], 'is', kwds['age'], 'years old')

def without_stars(kwds):
print(kwds['name'], 'is', kwds['age'], 'years old')

args = {'name': 'Mr. Gumby', 'age': 42}
with_stars(**args)
# 打印内容为 Mr. Gumby is 42 years old
without_stars(args)
# 打印内容为 Mr. Gumby is 42 years old

it may be useful to use these splicing operators to “pass through” parameters, without worrying too much about how many there are, and so forth.

1
2
3
4
5
6
def foo(x, y, z, m=0, n=0):
print(x, y, z, m, n)

def call_foo(*args, **kwds):
print("Calling foo!")
foo(*args, **kwds)

this can be particularly useful when calling the constructor of a superclass

Parameter Practice

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def story(**kwds):
return 'Once upon a time, there was a ' \
'{job} called {name}.'.format_map(kwds)

def power(x, y, *others):
if others:
print('Received redundant parameters:', others)
return pow(x, y)

def interval(start, stop=None, step=1):
'Imitates range() for step > 0'
if stop is None: # If the stop is not supplied ...
start, stop = 0, start # shuffle the parameters
result = []

i = start # We start counting at the start index
while i < stop: # Until the index reaches the stop index ...
result.append(i) # ... append the index to the result ...
i += step # ... increment the index with the step (> 0)
return result



Scoping

You can think of them as names referring to values. So, after the assignment x = 1, the name x refers to the value 1. It’s almost like using dictionaries, where keys refer to values, except that you’re using an “invisible” dictionary.

There is a built-in function called vars, which returns this dictionary:

This sort of “invisible dictionary” is called a namespace or scope.

what if you want to access the global variables inside a function? As long as you only want to read the value of the variable (that is, you don’t want to rebind it), there is generally no problem.

reading the value of global variables is not a problem in general, but one thing may make it problematic. if a local variable or parameter exists with the same name as the global variable you want to access, you can’t do it directly. the global variable is shadowed by the local one.

if needed, you can still gain access to the global variable by using the function globals, a close relative of vars, which returns a dictionary with the global variables. (locals returns a dictionary with the local variables.)

1
2
def combine(parameter): 
print(parameter + globals()['parameter'])

Rebinding global variables (making them refer to some new value) is another matter. If you assign a value to a variable inside a function, it automatically becomes local unless you tell Python otherwise.

1
2
3
def change_global(): 
global x
x = x + 1

NESTED SCOPES

nesting is normally not all that useful, but there is one particular application that stands out: using one function to “create” another. this means that you can (among other things) write functions like the following:

1
2
3
4
5
def multiplier(factor):
def multiplyByFactor(number):
return number * factor
return multiplyByFactor
# A function such as multiplyByFactor that stores its enclosing scopes is called a closure.

one function is inside another, and the outer function returns the inner one; that is, the function itself is returned — it is not called. What’s important is that the returned function still has access to the scope where it was defined; in other words, it carries its environment (and the associated local variables) with it!
each time the outer function is called, the inner one gets redefined, and each time, the variable factor may have a new value. because of python’s nested scopes, this variable from the outer local scope (of multiplier) is accessible in the inner function later

normally, you cannot rebind variables in outer scopes. if you want, though, you can use the nonlocal keyword. it is used in much the same way as global, and it lets you assign to variables in outer (but nonglobal) scopes.

Recursion

1
2
def recursion():
return recursion()

each time a function is called, it uses up a little memory, and after enough function calls have been made (before the previous calls have returned), there is no more room, and the program ends with the error message maximum recursion depth exceeded.

each time a function is called, a new namespace is created for that specific call. That means that when a function calls “itself,” you are actually talking about two different functions

Factorial and Power

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def factorial(n):
result = n
for i in range(1, n):
result *= i
return result

def factorial(n):
if n == 1:
return 1
else:
return n * factorial(n - 1)

def power(x, n):
# built-in function pow, or the operator **
result = 1
for i in range(n):
result *= x
return result

def power(x, n):
if n == 0:
return 1
else:
return x * power(x, n - 1)
1
2
3
4
5
6
7
8
9
10
11
12
def search(sequence, number, lower=0, upper=None):
if upper is None:
upper = len(sequence) - 1
if lower == upper:
assert number == sequence[upper]
return upper
else:
middle = (lower + upper) // 2
if number > sequence[middle]:
return search(sequence, number, middle + 1, upper)
else:
return search(sequence, number, lower, middle)

fucntional programming

using functions just like other objects (strings, number, sequences, and so on) by assigning them to variables, passing them as parameters, and returning them from other functions. some programming languages use functions in this way to accomplish almost everything.

python has a few functions that are useful for this sort of “functional programming”: map, filter, and reduce. the map and filter functions are not really all that useful in current versions of python, and you should probably use list comprehensions instead. You can use map to pass all the elements of a sequence through a given function.

1
list(map(str, range(10))) # Equivalent to [str(i) for i in range(10)]

use filter to filter out items based on a boolean function.

1
2
3
4
5
6
7
8
9
def func(x):
return x.isalnum()

seq = ["foo", "x41", "?!", "***"]
list(filter(func, seq))
# 结果为 ['foo', 'x41']

# using a list comprehension would mean you didn’t need to define the custom function.
[x for x in seq if x.isalnum()]

lambda expressions, which lets you define simple functions in-line (primarily used with map, filter, and reduce).

1
filter(lambda x: x.isalnum(), seq)

the reduce function cannot easily be replaced by list comprehensions, but you probably won’t need its functionality all that often

it combines the first two elements of a sequence with a given function, combines the result with the third element, and so on, until the entire sequence has been processed and a single result remains.

1
2
3
4
numbers = [72, 101, 108, 108, 111, 44, 32, 119, 111, 114, 108, 100, 33]
from functools import reduce
reduce(lambda x, y: x + y, numbers)
# 结果为1161

7. More Abstraction

making your own objects (and especially types or classes of objects) is a central concept in Python

核心概念: polymorphism and encapsulation, methods and attributes, superclasses, and inheritance

Objects

  • Polymorphism: You can use the same operations on objects of different classes, and they will work as if “by magic.”
  • Encapsulation: You hide unimportant details of how objects work from the outside world.
  • Inheritance: You can create specialized classes of objects from general ones.

Polymorphism

以例子说明:

For example, assume that you are creating an online payment system for a commercial web site that sells food. Your program receives a “shopping cart” of goods from another part of the system — all you need to worry about is summing up the total and billing some credit card.

Your first thought may be to specify exactly how the goods must be represented when your program receives them. (‘SPAM’, 2.50)For example, you may want to receive them as tuples, like this:

1
('SPAM', 2.50)

If all you need is a descriptive tag and a price, this is fine. But it’s not very flexible. Let’s say that some clever person starts an auctioning service as part of the web site — where the price of an item is gradually reduced until someone buys it. It would be nice if the user could put the object in her shopping cart, proceed to the checkout (your part of the system), and just wait until the price was right before clicking the Pay button.

But that wouldn’t work with the simple tuple scheme. For that to work, the object would need to check its current price (through some network magic) each time your code asked for the price—it couldn’t be frozen like in a tuple. You can solve that by making a function.

1
2
3
4
5
6
7
8
# Don't do it like this ... 
def get_price(obj):
if isinstance(obj, tuple):
return obj[1]
elif isinstance(obj, dict):
return int(obj['price'])
else:
return magic_network_method(obj)

Every new object type can retrieve or calculate its own price and return it to you—all you have to do is ask for it. And this is where polymorphism

All you know is that you can ask for its price, and that’s enough for you.

1
obj.get_price()

Many functions and operators are polymorphic — probably most of yours will be, too, even if you don’t intend them to be. Just by using polymorphic functions and operators, the polymorphism “rubs off.” In fact, virtually the only thing you can do to destroy this polymorphism is to do explicit type checking with functions such as type or issubclass. If you can, you really should avoid destroying polymorphism this way. What matters should be that an object acts the way you want, not whether it is of the right type (or class). The injunction against type checking is not as absolute as it once was, however. With the introduction of abstract base classes and the abc module, discussed later in this chapter, the issubclass function itself has become polymorphic!

Encapsulation

  • Polymorphism enables you to call the methods of an object without knowing its class (type of object).
  • Encapsulation enables you to use the object without worrying about how it’s constructed.

how can you “encapsulate” the name within the object? No problem. You make it an attribute.

Attributes are variables that are a part of the object, just like methods; actually, methods are almost like attributes bound to functions.

the object has its own state. The state of an object is described by its attributes

The methods of an object may change these attributes.

So it’s like lumping together a bunch of functions (the methods) and giving them access to some variables attributes) where they can keep values stored between function calls.

more details on Python’s encapsulation mechanisms in the section “Privacy Revisited” later in the chapter.

Inheritance

Inheritance is another way of dealing with laziness

Classes

All objects belong to a class and are said to be instances of that class.

Defining subclasses is then only a matter of defining more methods, or overriding some of the existing ones

In older versions of python, there was a sharp distinction between types and classes. built-in objects had types; your custom objects had classes. You could create classes but not types. In recent versions of python 2, this difference is much less pronounced, and in python 3, the distinction has been dropped.

get to make your own classes!

1
2
3
4
5
6
7
8
__metaclass__ = type    # Include this if you’re using Python 2
class Person:
def set_name(self, name):
self.name = name
def get_name(self):
return self.name
def greet(self):
print("Hello, world! I'm {}.".format(self.name))

there is a difference between so-called old-style and new-style classes. there is really no reason to use the old-style classes anymore, except that they’re what you get by default prior to python 3. to get newstyle classes in older pythons, you should place the assignment __metaclass__ = type at the beginning of your script or module. there are also other solutions, such as subclassing a new-style class (for example, object). You learn more about subclassing in a minute. If you’re using python 3, there is no need to worry about this, as old-style classes don’t exist there. You find more information about this in Chapter 9.

Attributes, Functions, and Methods

The self parameter is what distinguishes methods from functions.

Privacy Revisited

accessor methods, such as get_name and set_name.(In Chapter 9, you learn about properties, a powerful alternative to accessors.)

To make a method or attribute private (inaccessible from the outside), simply start its name with two underscores.

Inside a class definition, all names beginning with a double underscore are “translated” by adding a single underscore and the class name to the beginning. If you know how this works behind the scenes, it is still possible to access private methods outside the class, even though you’re not supposed to.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Secretive:
def __inaccessible(self):
print("Bet you can't see me ...")
def accessible(self):
print("The secret message is:")
self.__inaccessible()

s = Secretive()
s.__inaccessible()
"""
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: Secretive instance has no attribute '__inaccessible'
"""
Secretive._Secretive__inaccessible
"""
<unbound method Secretive.__inaccessible>
"""
s._Secretive__inaccessible()
"""
Bet you can't see me ...
"""

The Class Namespace

all the code in the class statement is executed in a special namespace — the class namespace.

This namespace is accessible later by all members of the class.

class definitions are simply code sections that are executed, but it can be useful information. For example, you aren’t restricted to def statements inside the class definition block.

1
2
class C:
print("...")

a variable is defined in the class scope, which can be accessed by all the members (instances), in this case to count the number of class members.

1
2
3
4
5
6
7
8
9
10
11
class MemberCounter:
members = 0
def init(self):
MemberCounter.members += 1

m1 = MemberCounter()
m1.init()
print(MemberCounter.members) # 1
m2 = MemberCounter()
m2.init()
print(MemberCounter.members) # 2

This class scope variable is accessible from every instance as well

1
2
m1.members # 2
m2.members # 2

What happens when you rebind the members attribute in an instance?

1
2
m1.members = "Two"
# 此时 m1.members 变为 "Two",但 m2.members 依然为 2

The new members value has been written into an attribute in m1, shadowing the class-wide variable. This mirrors the behavior of local and global variables in functions**

Investigating Inheritance

  • find out whether a class is a subclass of another — built-in method issubclass
  • have a class and want to know its base classes — special attribute bases
  • check whether an object is an instance of a class — isinstance

Using isinstance is usually not good practice. relying on polymorphism is almost always better. the main exception is when you use abstract base classes and the abc module.

isinstance also works with types, such as the string type (str).

If you just want to find out which class an object belongs to, you can use the __class__ attribute

If you have a new-style class, either by setting __metaclass__ = type or by subclassing object, you could also use type(s) to find the class of your instance. For old-style classes, type simply returns the instance type, regardless of which class an object is an instance of.

Multiple Superclasses

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Calculator:
def calculate(self, expression):
self.value = eval(expression)

class Talker:
def talk(self):
print('Hi, my value is', self.value)

class TalkingCalculator(Calculator, Talker):
pass

tc = TalkingCalculator()
tc.calculate('1 + 2 * 3')
tc.talk()
# Hi, my value is 7

The subclass (TalkingCalculator) does nothing by itself; it inherits all its behavior from its superclasses. — multiple inheritance

If you are using multiple inheritance, there is one thing you should look out for: if a method is implemented differently by two or more of the superclasses (that is, you have two different methods with the same name), you must be careful about the order of these superclasses (in the class statement). The methods in the earlier classes override the methods in the later ones.

If the superclasses share a common superclass, the order in which the superclasses are visited while looking for a given attribute or method is called the method resolution order (MRO) and follows a rather complicated algorithm.

Interfaces and Introspection

The “interface” concept is related to polymorphism. When you handle a polymorphic object, you only care about its interface (or “protocol”)—the methods and attributes known to the world.

  • you can check whether the required methods are present, and if not, perhaps do something else. — hasattr
  • you could even check whether the talk attribute was callable. — callable and getattr
  • the inverse of getattr is setattr
  • If you want to see all the values stored in an object, you can examine its __dict__ attribute.
  • And if you really want to find out what an object is made of, you should take a look at the inspect module.

Abstract Base Classes

The idea of explicitly specified interfaces, as found in many other languages, such as Java and Go, with some third-party modules providing various implementations. Eventually, though, the official Python solution came with the introduction of the abc module.

This module provides support for so-called abstract base classes. In general, an abstract class is simply one that can’t, or at least shouldn’t, be instantiated. Its job is to provide a set of abstract methods that subclasses should implement. Here’s a simple example:

1
2
3
4
5
6
from abc import ABC, abstractmethod

class Talker(ABC):
@abstractmethod
def talk(self):
pass

use @abstractmethod to mark a method as abstract — a method that must be implemented in a subclass.

If you’re using older versions of python, you won’t find the ABC class in the abc module. You then need to import ABCMeta instead and place the (indented) line __metaclass__ = ABCMeta at the beginning of the class definition, just below the class statement line. If you’re using python 3 prior to 3.4, you can also use Talker(metaclass=ABCMeta) instead of Talker(ABC).

The most basic property of an abstract class (that is, one with abstract methods) is that it has no instances.

1
2
3
4
5
6
7
8
9
10
11
Talker() 
"""
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: Can't instantiate abstract class Talker with abstract methods talk
"""

# We haven’t overridden the talk method,
# so this class is also abstract and cannot be instantiated
class Knigget(Talker):
pass

And this is one of the main uses of abstract base classes — and perhaps the only proper use of isinstance in Python: if we first check that a given instance is indeed a Talker, we can be confident that when we need it, the instance will have the talk method.

1
2
3
4
5
6
7
8
9
class Knigget(Talker):
def talk(self):
print("Ni!")

k = Knigget()
isinstance(k, Talker)
# True
k.talk()
# Ni!

the abstract base class mechanism lets us use this kind of instance checking in the spirit of duck typing! We don’t care what you are — only what you can do. So if you implement the talk method but aren’t a subclass of Talker, you should still pass our type checking

1
2
3
4
5
6
7
class Herring:
def talk(self):
print("Blub.")

h = Herring()
isinstance(h, Talker)
# False

you could simply subclass Talker and be done with it, but you might be importing Herring from someone else’s module, in which case that’s not an option. Rather than creating a subclass of both Herring and Talker, you can simply register Herring as a Talker, after which all herrings are properly recognized as talkers.

1
2
3
4
5
6
Talker.register(Herring) 
# <class '__main__.Herring'>
isinstance(h, Talker)
# True
issubclass(Herring, Talker)
# True

There is a potential weakness here, though, that undermines the guarantees we saw when directly subclassing an abstract class.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Clam: 
pass

Talker.register(Clam)
# <class '__main__.Clam'>
issubclass(Clam, Talker)
# True
c = Clam()
isinstance(c, Talker)
# True
c.talk()
"""
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Clam' object has no attribute 'talk'
"""

The standard library provides several useful abstract base classes, for example in the collections.

Some Thoughts on Object-Oriented Design

  • Gather what belongs together. If a function manipulates a global variable, the two of them might be better off in a class, as an attribute and a method.
  • Don’t let objects become too intimate. Methods should mainly be concerned with the attributes of their own instance. Let other instances manage their own state.
  • Go easy on the inheritance, especially multiple inheritance. Inheritance is useful at times but can make things unnecessarily complex in some cases. And multiple inheritance can be very difficult to get right and even harder to debug.
  • Keep it simple. Keep your methods small. As a rule of thumb, it should be possible to read (and understand) most of your methods in, say, 30 seconds. For the rest, try to keep them shorter than one page or screen.

When determining which classes you need and which methods they should have, you may try something like this:

  1. Write down a description of your problem (what should the program do?). Underline all the nouns, verbs, and adjectives.
  2. Go through the nouns, looking for potential classes.
  3. Go through the verbs, looking for potential methods.
  4. Go through the adjectives, looking for potential attributes.
  5. Allocate methods and attributes to your classes.

Now you have a first sketch of an object-oriented model. You may also want to think about what responsibilities and relationships (such as inheritance or cooperation) the classes and objects will have. To refine your model, you can do the following:

  1. Write down (or dream up) a set of use cases—scenarios of how your program may be used. Try to cover all the functionality.
  2. Think through every use case step by step, making sure that everything you need is covered by your model. If something is missing, add it. If something isn’t quite right, change it. Continue until you are satisfied.

Share