[Advanced Python] 11 - Implement a Class

基礎概念:[Python] 08 - Classes --> Objectshtml

進階概念:[Advanced Python] 11 - Implement a Classpython

參考資源:廖雪峯,面向對象編程 linux

參考資源:廖雪峯,面向對象高級編程c++

參考資源:錯誤、調試和測試數據庫

 

 

考點


1、定義核心數據結構

不妨考慮下__init__中採用 DataFrame結構。編程

 

2、偏向於使用「函數」

  一個類實例也能夠成爲相似函數這樣能直接調用的對象,只要定義的時候有__call__()方法就能夠。設計模式

 

3、類屬性的添加限制

__slots__ = (...),減小沒必要要的靈活性從而節省內存空間。安全

 

4、@classmethod處理多態問題

類內的 類方法和實例方法的設計與實現。數據結構

 

5、其餘考點

多繼承、元類、異常處理、單元測試。多線程

 

6、代碼分析

class datetime的設計和實現。

 

 

 

類設計之考慮因素


1、限制"動態添加屬性"

Python做爲動態語言,類的屬性能夠動態添加,是否是太靈活了呢?那就限制一下屬性的添加範圍。

__slot__關鍵字

因爲'score'沒有被放到__slots__中,因此不能綁定score屬性,試圖綁定score將獲得AttributeError的錯誤。

注意:使用__slots__要注意,__slots__定義的屬性僅對當前類實例起做用,對繼承的子類是不起做用的:

class Student(object):
    __slots__ = ('name', 'age') # 用tuple定義容許綁定的屬性名稱


>>> s = Student() # 建立新的實例
>>> s.name = 'Michael' # 綁定屬性'name'
>>> s.age = 25 # 綁定屬性'age'
>>> s.score = 99 # 綁定屬性'score'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Student' object has no attribute 'score'

 

爲什麼用?

動態語言意味着「浪費空間」換取「靈活性」。顯然,減小沒必要要的靈活性能夠 reduce memory of RAM

From: 10. __slots__ Magic

使用後帶來的內存環保效果以下:

It involves the usage of __slots__ to tell Python not to use a dict, and only allocate space for a fixed set of attributes. Here is an example with and without __slots__:

Without __slots__:

class MyClass(object):
    def __init__(self, name, identifier):
        self.name = name
        self.identifier = identifier
        self.set_up()
    # ...

With __slots__:

class MyClass(object):
    __slots__ = ['name', 'identifier']   def __init__(self, name, identifier):
        self.name = name
        self.identifier = identifier
        self.set_up()
    # ...

The second piece of code will reduce the burden on your RAM. Some people have seen almost 40% to 50% reduction in RAM usage by using this technique.

 

 

2、類方法

原文連接:http://www.javashuo.com/article/p-wvqvpqvy-ks.html

(a) 與 「靜態方法」

Ref: python中@classmethod @staticmethod區別

    • staticmethod
    • classmethod   --> 表明類自己的參數:cls;表明對象自己的參數:self
class Person(object):
    
    grade=6     # 類變量
    
    def __init__(self):
        self.name = "king"
        self.age=20
----------------------------------------------------------------------- def sayHi(self): #加self區別於普通函數 print ('Hello, your name is?',self.name) def sayAge(self): print( 'My age is %d years old.'%self.age) ----------------------------------------------------------------------- @staticmethod # 靜態方法不能訪問類變量和實例變量,也不能使用self def sayName(): print ("my name is king") @classmethod # 類方法能夠訪問類變量,但不能訪問實例變量 def classMethod(cls): #print('cls:',cls) print('class grade:',cls.grade) #print("class method") class Child(object): def __init__(self): self.name = "小明" self.age=20 def sayName(self, obj): print('child name is:', self.name) print(obj.sayName) print(obj.sayName())   # 這裏要特別注意帶括號和不帶括號的區別:一個是對象,一個是方法 p = Person() # 實例化對象 print('p.grade:',p.grade) # 實例對象調用類變量 p.grade=9 print(p.classMethod(), p.grade) # 實例改變類變量時,其grade變量只會在實例中改變 print(Person().grade) # 類對象調用類變量 p.sayHi() # 實例對象調用類成員函數 Person().sayAge() # 類對象調用類成員函數 p.sayName() # 實例對象調用類靜態方法 m=Person() m.sayName() # 多個實例共享此靜態方法 Person().sayName() # 類對象調用靜態方法 p.classMethod() # 實例對象調用類方法 Person.classMethod() # 類對象調用類方法 # 調用類 tt=Child() tt.sayName(Person())

 

什麼時候使用?

Ref: class method vs static method in Python 

    • We generally use class method to create factory methods. Factory methods return class object ( similar to a constructor ) for different use cases.
    • We generally use static methods to create utility functions.

爲什麼這裏提到了」工廠方法「?由於工廠方法須要返回類,而類方法是'天生的」具有cls參數。

而靜態方法即便在類中,也沒法達到被繼承的效果。  

Ref: Python classmethod()

from datetime import date

# random Person
class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    @classmethod def fromBirthYear(cls, name, birthYear):
        return cls(name, date.today().year - birthYear)

    def display(self):
        print(self.name + "'s age is: " + str(self.age))


# (1) 常規方式返回一個實例對象 person
= Person('Adam', 19) person.display()
# (2) 類方法返回一個實例對象 person1
= Person.fromBirthYear('John', 1985) person1.display()

 

動態變爲類方法

類函數 ----> classmethod

printAge本來是個普通函數,經過classmethod變爲類方法。

class Person:
    age = 25

    def printAge(cls):
        print('The age is:', cls.age)

# create printAge class method
Person.printAge = classmethod(Person.printAge)
Person.printAge()

 

(b) 與 "實例方法"

須要注意下第一種方法,沒有self 以及 cls,實例對象不能調用的一種「類方法」。

class Person(object):
    
########################################
# 類方法的 叄種 定義方式
########################################
    # [第壹種方法] 不加任何參數直接定義,也是類方法 
def Work():
print(" I am working!")
# [第貳種方法] 加裝飾器方法 @classmethod def Think(cls, b): #類方法Think必需要帶至少1個參數,第一個參數默認爲類名,後面能夠引用。 cls.Eat(b) #在類方法Think中,調用類方法Eat類方法。 cls.Work() #在類方法Think中,調用Work類方法。 print(b,",I am Thinking!") # [第叄種方法] 先定義類方法,至少1個參數,第一個默認爲類名 def Eat(cls, b): print(b+",I am eating") Eat=classmethod(Eat) #第二種方法:經過內建函數classmethod()來建立類方法。
-----------------------------------------------------------------------------------
# 靜態方法,引用時直接用類名.Sleep()便可。 @staticmethod def Sleep(): print("I am sleeping") # 這種方法是:實例對象調用方法 def __scolia__(self): print("scola") return "scola"
-----------------------------------------------------------------------------------
# 實例對象能夠訪問的私有方法,在類方法中能夠相互調用和使用。類不能直接訪問或者外部訪問 def __good(self): print("good") return "good" Person.Think("li") Person.Eat("jcy") Person.Work() # a.Work() 報錯,實例對象不能調用類方法 Person.Sleep() a=Person() a.__colia__() # 魔術方法,沒有私有化。 #a.__good() # 私有方法:報錯了!

 

類方法de類外實現

Yes. You can define a function outside of a class and then use it in the class body as a method.

link: https://stackoverflow.com/questions/9455111/define-a-method-outside-of-class-definition

def func(self):
    print("func")

class MyClass:
    myMethod = func
 

 

3、多繼承歧義 

歧義

1. 倒三角狀況,只需「括號中排前面的父類的方法」優先,便可解決。

2. 鑽石繼承時,c++中採用虛函數解決"路徑歧義性」, goto: [c++] Class,那 python 怎麼辦? 

class Father(object):
    def __init__(self, name):
        self.name = name
        print("Im father")

class Son_1(Father):
    def __init__(self, age, name):
        self.age = age
        Father.__init__(self, name)
        print("Im Son_1")


class Son_2(Father):
    def __init__(self, gender, name):
        self.gender = gender
        Father.__init__(self, name)
        print("我是Son_2")


class GrandSon(Son_1, Son_2):
    def __init__(self, name, age, gender):
        Son_1.__init__(self, age, name)      # init兩次,看上去也很怪
        Son_2.__init__(self, gender, name)
        pass
grand_son = GrandSon("張三", 18, "") print(GrandSon.__mro__)

Father類被執行了兩次!有問題。 

Im father
Im Son_1
Im father
我是Son_2
(<class '__main__.GrandSon'>, <class '__main__.Son_1'>, <class '__main__.Son_2'>, <class '__main__.Father'>, <class 'object'>)

 

解決辦法

Ref:多繼承(鑽石繼承)的問題和解決

使用虛基類和虛繼承可讓一個指定的基類在繼承體系中將其成員數據實例共享給從該基類直接或間接派生出的其它類,即便從不一樣路徑繼承來的同名數據成員在內存中只有一個拷貝,同一個函數名也只有一個映射。

 

利用super()調用父類的__init__

用super()確實可以解決父類__init__重複調用的問題,可是這個傳參是個麻煩,這樣一來,Son1和Son2的參數須要根據grandSon的參數來變化,並且不能寫死,這樣很是不合理。

參數的順序是要保持一致的,這個限制條件看上去不是很「和諧」,須要繼續改進。

class Father(object):
    def __init__(self, name):
        self.name = name
        print("Im father")


class Son_1(Father):
    def __init__(self, name, age, gender):
        self.age = age
        super(Son_1, self).__init__(name, gender)
        print("Im Son_1")


class Son_2(Father):
    def __init__(self, name, gender):
        self.gender = gender
        super(Son_2, self).__init__(name)
        print("我是Son_2")


class GrandSon(Son_1, Son_2):
    def __init__(self, name, age, gender):
        super(GrandSon, self).__init__(name, age, gender)
print("我是GrandSon")
grand_son
= GrandSon("張三", 19, "",) print(GrandSon.__mro__)

 

使用「參數星號表達」

class Father(object):
    def __init__(self, name, *args, **kwargs):
        self.name = name
        print("我是父類__init__")


class Son_1(Father):
    def __init__(self, name, age, *args, **kwargs):
        print("我是Son_1的__init__")
        super(Son_1, self).__init__(name, *args, **kwargs)
        self.age = age


class Son_2(Father):
    def __init__(self, name, gender, *args, **kwargs):
        print("我是Son_2的__init__")
        self.gender = gender
        super(Son_2, self).__init__(name, *args, **kwargs)


class GrandSon(Son_1, Son_2):
    def __init__(self, name, age, gender):
        super(GrandSon, self).__init__(name, age, gender)
def say_hello(self): print(self.name, self.age, self.gender)
grand_son
= GrandSon("老王", 24, "")

Output:

Python 3.7.4 (default, Jul  9 2019, 00:06:43)
[GCC 6.3.0 20170516] on linux
我是Son_1的__init__
我是Son_2的__init__
我是父類__init__

 

 

4、使用元類

Ref: 使用元類

type()

不只能夠動態建立一個「對象」,也能夠建立一個「類」。

type()函數既能夠返回一個對象的類型,又能夠建立出新的類型,好比,咱們能夠經過type()函數建立出Hello類,而無需經過class Hello(object)...的定義:

>>> def fn(self, name='world'): # 先定義函數
...     print('Hello, %s.' % name)
...
>>> Hello = type('Hello', (object,), dict(hello=fn)) # 建立Hello class >>> h = Hello() >>> h.hello() Hello, world.
>>> print(type(Hello)) <class 'type'> >>> print(type(h)) <class '__main__.Hello'>

 

metaclass

邏輯關係

控制類的建立行爲,還可使用metaclass。

先定義metaclass,就能夠建立類,最後建立實例。

 

應用價值

More details: https://www.liaoxuefeng.com/wiki/1016959663602400/1017592449371072

動態修改有什麼意義?直接在MyList定義中寫上add()方法不是更簡單嗎?正常狀況下,確實應該直接寫,經過metaclass修改純屬變態。

可是,總會遇到須要經過metaclass修改類定義的。ORM就是一個典型的例子。

ORM全稱「Object Relational Mapping」,即對象-關係映射,就是把關係數據庫的一行映射爲一個對象,也就是一個類對應一個表,這樣,寫代碼更簡單,不用直接操做SQL語句。

要編寫一個ORM框架,全部的類都只能動態定義,由於只有使用者才能根據表的結構定義出對應的類來。

 

 

5、輸入異常處理

一些要點

手動緩衝讀取文件

 with open(self.file_path) as file:
  while 1:
    lines = file.readlines(1000)     if not lines:
      break
    for line in lines:
      items = line.strip().split('|')
      print(itmes)

 

異常 - 非有效數字 string

try:
  weekday = self.fnWhichWeekday(str_weekday)
  y = int(str_y)
  yhat = int(str_yhat)
  print("weekday: {}, y: {}, yhat: {}".format(weekday, y, yhat))
except Exception as e:
  print("Invaid line.")
  continue;

 

異常 - 分母爲零

try:
  F1 = 2.0/((1.0/self.PRECISION)+(1.0/self.RECALL))
except ZeroDivisionError as e:
  print("ZeroDivisionError", e)
  print("Debug03: PRECISION: {}, RECALL: {}".format(self.PRECISION, self.RECALL))

 

Code Show

  1 from datetime import datetime
  2 import re
  3 
  4 class CalculatorML:
  5 
  6     TP=0
  7     TN=0
  8     FP=0
  9     FN=0
 10     PRECISION=0
 11     RECALL=0
 12 
 13     def __init__(self, file_path, start_line):
 14         self.file_path = file_path
 15         self.start_line = start_line
 16 
 17 
 18     def fnWhichWeekday(self, text):
 19         match = re.search(r'\d{4}-\d{2}-\d{2}', text)          # 找到時間字符串
 20         date = datetime.strptime(match.group(), '%Y-%m-%d').date()   # 時間格式轉換
 21         weekday = date.weekday()
 22         return weekday
 23 
 24 
 25     def fnScanLines(self):
 26         with open(self.file_path) as file:
 27             while 1:
 28                 lines = file.readlines(1000)
 29                 if not lines:
 30                     break
 31                 for line in lines:
 32                     items = line.strip().split('|')
 33 
 34                     # valid line.
 35                     if len(items) is not 3:
 36                         continue
 37 
 38                     str_weekday = items[0]
 39                     str_y       = items[1]
 40                     str_yhat    = items[2]
 41 
 42                     # valid item.
 43                     try:
 44                         weekday = self.fnWhichWeekday(str_weekday)
 45                         y = int(str_y)
 46                         yhat = int(str_yhat)
 47                         print("weekday: {}, y: {}, yhat: {}".format(weekday, y, yhat))
 48                     except Exception as e:
 49                         print("Invaid line.")
 50                         continue;
 51 
 52                     # TP
 53                     if (y is yhat) and (y is 1):
 54                         self.TP += 1
 55                     # TN
 56                     if (y is not yhat) and (y is 1):
 57                         self.TN += 1
 58                     # FP
 59                     if (y is yhat) and (y is 0):
 60                         self.FP += 1
 61                     # FN
 62                     if (y is not yhat) and (y is 0):
 63                         self.FN += 1
 64 
 65         print("Finally, TP: {}, TN: {}, FP: {}, FN: {}".format(self.TP, self.TN, self.FP, self.FN))
 66 
 67 
 68     def fnSetPrecision(self):
 69         try:
 70             self.PRECISION=(self.TP*1.0)/(self.TP+self.FP)
 71         except ZeroDivisionError as e:
 72             print("ZeroDivisionError", e)
 73             print("Debug01: TP: {}, TP: {}, FP: {}".format(self.TP, self.TP, self.FP))
 74 
 75     def fnSetRecall(self):
 76         try:
 77             self.RECALL=(self.TP*1.0)/(self.TP+self.FN)
 78         except ZeroDivisionError as e:
 79             print("ZeroDivisionError", e)
 80             print("Debug02: jTP: {}, TP: {}, FN: {}".format(self.TP, self.TP, self.FN))
 81 
 82     def fnGetF1(self):
 83 
 84         self.fnScanLines()
 85         self.fnSetPrecision()
 86         self.fnSetRecall()
 87 
 88         try:
 89             F1 = 2.0/((1.0/self.PRECISION)+(1.0/self.RECALL))
 90 
 91         except ZeroDivisionError as e:
 92             print("ZeroDivisionError", e)
 93             print("Debug03: PRECISION: {}, RECALL: {}".format(self.PRECISION, self.RECALL))
 94 
 95         return F1;
 96 
 97 
 98 filePath = "./test.psv"
 99 start_line = 3
100 
101 calculator = CalculatorML(filePath, start_line)
102 F1 = calculator.fnGetF1()
103 print(F1)
判斷數字有效性

 

 

6、測試

程序能一次寫完並正常運行的機率很小,基本不超過1%。總會有各類各樣的bug須要修正。

[Optimized Python] 17 - Performance bottle-neck

 

 

 

實戰分析


1、找到一個官方文件

In [52]: import datetime                                                                                                           

In [53]: datetime.__file__                                                                                                         
Out[53]: '/usr/local/anaconda3/lib/python3.7/datetime.py'

這裏,datetime繼承了date類,是一個不錯的分析案例。

class datetime(date):
    """datetime(year, month, day[, hour[, minute[, second[, microsecond[,tzinfo]]]]])

    The year, month and day arguments are required. tzinfo may be None, or an
    instance of a tzinfo subclass. The remaining arguments may be ints.
    """
    __slots__ = date.__slots__ + time.__slots__

    def __new__(cls, year, month=None, day=None, hour=0, minute=0, second=0,
                microsecond=0, tzinfo=None, *, fold=0):
if (isinstance(year, (bytes, str)) and len(year) == 10 and 1 <= ord(year[2:3])&0x7F <= 12): # Pickle support if isinstance(year, str): try: year = bytes(year, 'latin1') except UnicodeEncodeError: # More informative error message. raise ValueError( "Failed to encode latin1 string when unpickling " "a datetime object. " "pickle.load(data, encoding='latin1') is assumed.") self = object.__new__(cls) self.__setstate(year, month) self._hashcode = -1 return self
year, month, day
= _check_date_fields(year, month, day) hour, minute, second, microsecond, fold = _check_time_fields( hour, minute, second, microsecond, fold) _check_tzinfo_arg(tzinfo) self = object.__new__(cls) self._year = year self._month = month self._day = day self._hour = hour self._minute = minute self._second = second self._microsecond = microsecond self._tzinfo = tzinfo self._hashcode = -1 self._fold = fold return self

date類大概分爲以下幾個部分:

類註釋

__slots__

def __new__

@classmethod

__str__

@property

類運算符重載

 

 

2、新特性:__new__

Ref: python的__new__方法

最重要的「有返回值」,就是返回了self。

class Person(object):

    def __new__(cls, name, age):
        print '__new__ called.'
        return super(Person, cls).__new__(cls, name, age)

    def __init__(self, name, age):
        print '__init__ called.'
        self.name = name
        self.age = age

    def __str__(self):
        return '<Person: %s(%s)>' % (self.name, self.age)

if __name__ == '__main__':
    name = Person('xxx', 24)
    print name

 

何時須要__new__方法。

new方法主要是當你繼承一些不可變的class時(好比int, str, tuple), 提供給你一個自定義這些類的實例化過程的途徑。還有就是實現自定義的metaclass。

具體咱們能夠用int來做爲一個例子:這是一個不可變的類,但咱們還想繼承它的優良特性,怎麼辦?

class PositiveInteger(int):
def __new__(cls, value): return super(PositiveInteger, cls).__new__(cls, abs(value)) i = PositiveInteger(-3) print i

 

3、單例模式

隨着編程語言的演進,一些設計模式(如單例)也隨之過期,甚至成了反模式(請參考網頁[ t.cn/zRoxwyD]),另外一些則被內置在編程語言中(如迭代器模式)。

另外,也有一些新的模式誕生(好比Borg/Monostate,請參考網頁[ t.cn/zWOOOZC]和[ t.cn/RqrKbBe])

 

單例模式,須要在類的」建立部分「作一些手腳,也就天然和__new__扯上關係。

class Singleton(object):
    __instance = None
    __first_init = True

    def __new__(cls, age, name):
        if not cls.__instance:
            cls.__instance = object.__new__(cls)
        return cls.__instance

    def __init__(self, age, name):
        if self.__first_init:
            self.age  = age
            self.name = name
            Singleton.__first_init = False


a = Singleton(18, "xxx")
b = Singleton(8, "xxx")  # (1) 實際上是同一個對象 print(id(a))  # (2) 此處相等,也說明了是"同一個對象" print(id(b))


print(a.age)
print(b.age)  # (3) 由於是同一個對象,即便b的__init__沒作什麼,b依然能夠拿來去用,由於b實際上就是a的引用

a.age = 19
print(b.age)

輸出:

139953926130600
139953926130600
18
18
19

 

但,慎用單例模式!

對程序架構而言,單例意味着沒有隱藏,插入到程序的任何組件均可以隨時修改它,這客觀上違背了面向對象的最小公開法則,程序健壯性安全性驟降。
對程序擴展性而言,單例意味着很難被繼承重寫!  當你在一個單例中嘗試覆蓋它的某些功能時,編譯器會報錯,這時候你哭去吧。或者,你會奇怪的發現,咦?怎麼仍是基類的功能!
對程序邏輯而言,單例,顧名思義,僅有其一,但你的程序在發展着,你肯定只有一個實例便可?某天忽然發現,業務變了,須要擴展,那些數不清的用單例調用的代碼怎麼處理?尤爲是已經發布到顧客手中的代碼?
對效率而言,每次訪問它的時候,都會查看是否被建立(多增長了一次判斷),這對於一些須要高性能的程序是很是不合適的。
對多線程而言,在多線程環境下,實現單例是須要技巧的。不然,單例不單!
對對象生命週期而言,單例只有本身持有真正的引用,,如何銷燬什麼時候銷燬都是問題,可能還會形成指針懸掛。
對開發者而言,一個類不能new! 這還會帶來更多的疑問和混淆。

 

4、工廠方法之classmethod

內部實現

類方法,這裏返回的都是類。有必要麼?爲何?

 @classmethod def fromtimestamp(cls, t):
        "Construct a date from a POSIX timestamp (like time.time())."
        y, m, d, hh, mm, ss, weekday, jday, dst = _time.localtime(t)
        return cls(y, m, d)

    @classmethod def today(cls):
        "Construct a date from time.time()."
        t = _time.time()
        return cls.fromtimestamp(t)

    @classmethod def fromordinal(cls, n):
        """Construct a date from a proleptic Gregorian ordinal.

        January 1 of year 1 is day 1.  Only the year, month and day are
        non-zero in the result.
        """
        y, m, d = _ord2ymd(n)
        return cls(y, m, d)

    @classmethod def fromisoformat(cls, date_string):
        """Construct a date from the output of date.isoformat()."""
        if not isinstance(date_string, str):
            raise TypeError('fromisoformat: argument must be str')

        try:
            assert len(date_string) == 10
            return cls(*_parse_isoformat_date(date_string))
        except Exception:
            raise ValueError(f'Invalid isoformat string: {date_string!r}')

 

使用案例

爲什麼返回一個類?

—— 由於暫時還不清楚user的結果細節,例如什麼格式的結果。

import datetime

end_time = 1525104000000
d = datetime.datetime.fromtimestamp(end_time / 1000, None)  # 時間戳轉換成字符串日期時間,返回的實際上是一個類!

str1 = d.strftime("%Y-%m-%d %H:%M:%S.%f")
print(d) # 2018-05-01 00:00:00 print(str1) # 2018-05-01 00:00:00.000000

 

對比c++,能夠經過」函數重載「 (function overloading)。但python對於構造函數而言,能夠經過 @classmethod 補充這個效果。

 

End.

相關文章
相關標籤/搜索