理解Ruby中的類

live with scopepython

序言

源起於Python開發者'公衆號轉載的深入理解Python中的元類一文, 回憶着本身看過的 Ruby元編程 一書, 參照寫個相應的Ruby版.編程

Python和Ruby在不少方面都很是相像, 特別是Ruby的設計部分參考了Python. 但在不少方面, 它倆的不少概念並不是一一對應的. 在這裏的 元類 在Ruby 中並無相應的概念, 若是理解爲建立類的類, 最相近的應該是Class .api

這裏不會將那篇文章的內容都複製過來, 只是挑選不同的地方寫一寫, 所以, 你最好已經讀過那篇文章了. 讀這篇時, 最好對照着讀.ruby

類也是對象

相比Python, Ruby語言有着最純粹的面向對象編程的設計. 一樣的,Ruby的類的概念也是借鑑於Smalltalk. 關於什麼是類, 我更傾向於理解爲, 描述一個對象的狀態(實例變量)和操做(方法)的代碼段.函數

class ObjectCreator < Object; end  #=> nil
my_object = ObjectCreator.new  #=>#<ObjectCreator:0x00000000b41400>
print my_object  #=>nil

說明:翻譯

  • 類默認繼承自Object, 所以< Object非必要. 原文的Python代碼也是.設計

  • 在Ruby 中, 在不引發歧義的前提下, 函數調用的()能夠省略. 這裏同原文的Python 代碼雖然看起來相同, 但原理徹底不一樣.Python2.7中, print實現爲語句, 但在Python 3.x中, 實現爲全局函數, 則必須加()表示調用.code

  • 這裏的#=>表示輸出的結果, print無輸出, 即nil來表示無對象

同Python, Ruby的類一樣也是對象. 不一樣於Python, Ruby中的class實際是打開一個類, 若是類不存在則建立它. 換句話說, 在Python中重複class定義同一類, 後者會覆蓋前者, 而在Ruby中, 類是同一個, 後者只是給這個類添加了新的方法或變量. 繼承

Python:

class N1:
  def __init__(self, name):
    self.name = name
  def hello(self, s):
    return self.name + s
N1("lzp").hello(" is good man")  #=> "lzp is good man"
class N1:
  def __init__(self, name):
    self.name = name
  def world(self, s):
    return self.name + s
N1("lzp").hello(" is good man")  #=> AttributeError, 無屬性
N1("lzp").world(" is good man")  #=> "lzp is good man"

Ruby:

class N1
  def initialize(name)
    @name = name
  end
  def hello(s)
    @name + s
  end
end
N1.new("lzp").hello(" is good man")  #=> "lzp is good man"
class N1
  def initialize(name)
    @name = name
  end
  def world(s) @name + s end
end
N1("lzp").hello(" is good man")  #=> "lzp is good man"
N1("lzp").world(" is good man")  #=> "lzp is good man"
  • Ruby少了無語的self, 但多了無語的end.

  • Ruby的函數默認返回最後一個表達式的值, 但在Python中則必須顯示地return.

  • Ruby的方法定義能夠寫成一行,Python來咬我啊

不少語言都聲稱 _xx語言中一切都是對象_, 包括Java. 很明顯, 不一樣語言中的對象概念應該是有區別的, 那麼如何來理解對象呢. 這裏我基本贊成原文中所說, 可賦值, 可拷貝, 可增長屬性, 可做參傳遞.

注意, 不要將對象和對象的引用混淆, 對象的引用每每表現爲常見的各類標識符.

Rb: ObjectCreator.to_s  #=> "ObjectCreator"
Py: str(ObjectCreator)  #=> <class '__main__.ObjectCreator'>

因爲print函數實際是調用對象轉字符串後輸出, 並沒有特殊意義. 下面的例子更好地展現了, 做參傳遞.

Python:

def new(o):  return o()
oc1 = new(ObjectCreator)  #=><__main__.ObjectCreator at 0x...>, 新的實例對象

Ruby:

def new(o) o.new end
oc1 = new(ObjectCreator)  #=>#<ObjectCreator:0x...>, 新的實例對象

Python屬性操做

Python 中有3個全局函數, 用於對象的屬性操做.

  • hasattr(obj, 'attr_name')判斷對象是否有此屬性,

  • getattr(obj, 'attr_name')獲取對象指定屬性,

  • setattr(obj, 'attr_name', attr_value)則是設置指定屬性

  • obj.new_attr = attr_value設置屬性.

  • delattr(obj, 'attr_name')刪除屬性.

Python中的屬性是一個寬泛的概念, 包括類變量, 實例變量, 類方法和實例方法. 這其中的區別是很是經典的, 且在不一樣語言中有不一樣的名稱, 有不一樣的書面寫法.

  • 類變量, 一般指依附於類自己而非類的實例的變量, 表述的是類的狀態

  • 實例變量, 類的每一個實例有獨立的變量, 來表述實例對象的狀態

  • 類方法, 經過類名調用的方法

  • 實例方法, 經過類的實例對象調用的方法

在Python中, 經過給self.var_name賦值建立實例變量, 類定義中方法外賦值的非self變量都是類變量. 定義方法時, 傳遞有self參數的是實例方法, 不然爲類方法.

Python:

class N2:
  class_var = 3  # 類變量, 也能經過實例對象訪問
  def __init__(self, name):
    self.name = name  #實例變量
  def hello(self, s):
    return "hello " + self.name + s
  def world(s):
    return "world " + N2.c_var + s
n2 = N2("lzp")
n2.hello(" is good man")  #=> "hello lzp is good man"
N2.hello(n2, " is good man")  #=> "hello lzp is good man"
n2.world(" lzp")  #=> 函數只要參數, 但參數多餘
N2.world(" lzp")  #=> "world lzp"

Python在類的方法設計上很取巧. 就如以後所說, Python實際上是沒有類方法一說的, 所有都是函數. 類的方法第一個參數是self, 像在world方法定義中, 沒有self, 方法內是不能引用實例變量的. 且此處是否是self也無所謂, 任意標識符均可以, 基於慣例使用self. 且在對象上調用方法, 本質上只是將對象做爲接收者, 做爲第一參數傳遞給函數. 若函數的第一參數不是self, 則在對象上調用方法會提示多餘參數.

在Python中, 函數是對象, 同其餘全部對象同樣. 所以大一統的去理解Python的類概念就是: 類是對象, 對象有屬性, 屬性即變量名和其對應的對象. 若對應的對象是函數對象, 則對應的變量是函數名, 其中第一參數爲self的爲類實例方法.

從屬性的角度從新定義N2, Python:

class N2: pass
N2.c_var = 3
def init(self, name):  self.name = name
N2.__init__ = init
N2.hello = lambda self, s: "hello" + self.name + s
N2.world = lambda s: "world " + N2.c_var + s

這讓我想起了USB, 支持熱插拔, 即插即用, 想插就插,Python老爹真任性. 這裏使用了lambda來定義匿名函數.

Ruby屬性操做

Ruby沒有屬性一說, 但你也能夠去寬泛地去理解. 相反的,Ruby的類變量, 實例變量, 類方法和實例方法是清晰地分開的, 畢竟是純粹地面向對象. 另外一個,Ruby其實沒有函數一說, 全部函數都有其所屬的類, 沒有單獨的函數, 或者說Ruby只有方法. 關於屬性, 另外一個其餘面嚮對象語言中類似的概念是 _域_, 就是在類中佔塊地, 放變量仍是函數都行.

Ruby:

class N2
  @c_i_var = 1  #類的實例變量
  @@c_var = 3   #類變量, 子類可繼承
  def initialize(name)
    @name = name
  end
  def hello(s)
    "hello" + @name + s
  end
  def self.world(s)  #類方法
    "world " + @@c_var.to_s + s
  end
end
N2.new("lzp").hello(" is good man")  #=> "lzp is good man"
N2.world(" is good man") #=> "world 3 is good man"

在這裏, 類的實例變量能夠理解爲類做爲對象的實例變量. 實例變量是專屬於對象的. 而類變量則是屬於整個類體系的, 即它的全部子類均可以訪問.

回到原文, Python中的屬性對應Ruby的多個概念. 所以對屬性的操做也是分不一樣的在進行.

Ruby:

n1 = N1.new("lzp")
n1.instance_variables  # 返回全部實例變量
n1.instance_variable_set("@age", 3)  #=> 設置實例變量
n1.instance_variable_get("@age")  #=>實例變量
n1.instance_variable_defined?("@age")  #=> 判斷有無
n1.class_variables  # 返回全部類變量
n1.class_variable_get/set/defined?  #同上
N1.instance_methods(false)  # 列出全部非繼承的實例方法
N1.singleton_methods  # 列出全部非繼承的類方法

這裏的singleton_methods能夠理解爲類方法. 但嚴格地說, 它是專屬於對象的方法. 若專屬於類, 則成爲類方法. 換句話說,Ruby沒有類方法一說, 稱爲單件方法.

Ruby中, 一切皆對象. 所以有必要來理解下Ruby的對象模型, 詳細地建議看 _Ruby元編程_一書.

對象由狀態, 所屬類的引用和操做構成. 狀態和操做都是專屬的, 只能由本對象進行.運算. 普通對象的狀態即實例變量, 操做即單件方法, 類對象的狀態即類的實例變量即類變量, 類對象的操做即類的單件方法即類方法, 其實本質是相同的. 每一個對象都存儲有對所屬類的引用, 以此來知曉可調用的實例方法.

所謂所屬類的引用, 很簡單, 在對象上調用#class方法便可

1.class  #=> Fixnum
"1".class  #=> String
Fixnum.class  #=> Class
String.class  #=> Class

在後文會看到Python中相應的概念type.

動態地建立類

Ruby也能在函數中建立類.

def choose_class(name)
  if (name=='foo')
    Class.new {def hello "hello" end}
  else
    Class.new {def world "world" end}
  end
end
MyClass = choose_class('foo')
MyClass.new.hello  #=> "hello"

這裏不能使用原文中類似的class, 會提示不能在def中定義類. 不得不提早使用大招Class.new.

以前寫到String.classClass, 也就是說, 在Ruby中, 全部的類都是Class的對象. 注意大小寫. 天然, 建立新的類, 也就是建立Class的實例對象, 使用new操做, 同其餘全部類同樣. 不過建立的是匿名類, 賦值給一個首字母大寫的常量名便可.

a = Class.new
a.name  #=> nil
a.new.class  #=> xxx
A = a
A.name  #=> A
A.new.class  #=> A

好了, 原文進行到了Python的全部類的type都是type. 在本質上, 一切類的生成都是經過調用type進行的.

將上述Ruby代碼原樣翻譯過來, 對應的Python代碼爲:

def choose_class(name):
  if name=='foo':
    return type('Foo', (), {'hello': lambda self:"hello"})
  else:
    return type('Bar', (), {'world': lambda self:"world"})
MyClass = choose_class('foo')
MyClass().hello()  #=> "hello"

解釋下參數, 第一個是類名字符串, 第二個基類的元組,Python支持多繼承, 能夠有多個基類, 所謂的基類能夠理解爲超類, 父類等概念. 第三個是屬性, 由前所知, 類中的一切都是屬性. 如此便可定義一個新類.

但不一樣於Ruby, type的第一個參數即類名, 跟MyClass無關, 即賦值不會改變類名. 但Ruby是在將類對象第一次賦值給常量時生成類名的, 以後賦值也不會改變.

在Ruby中, Class.new(superclass)來表示繼承類.Ruby中只支持單繼承, 經過模塊來添加不一樣的功能.

前文提到,Ruby的類有打開性質, 給類添加方法和變量是很是方便.

到底什麼是元類

這裏須要先普及幾個經常使用的操做:

Python:

a = 1
a.__class__  #=> int, 對象的類
type(1)  #=> int
int.__base__  #=> object, 類的基類
int.__bases__  #=> (object,), 類的基類元組

Ruby:

1.class  #=> Fixnum, 對象的類
Fixnum.superclass  #=> Integer, 類的超類
Fixnum.ancestors  #=> [Fixnum, Integer, Numeric, Comparable, Object, Kernel, BasicObject], 類的祖先鏈

所謂祖先鏈, 即類, 類的超類, 類的超類的超類, ...一直到最初始的類, 即BasicObject. 其實, 在1.9以前, 全部類都是繼承自Object, 後來又在前面加入了BasicObject, 我的猜想是爲了所謂潔淨室技術吧.

原文提到, 不斷地調用.__class__屬性, 最終會到達type類型 ,Ruby中對應的, 不斷調用.class方法, 最終會到達Class類型. 原文中能夠從type繼承, 來建立元類. 但在Ruby中是不能建立Class的子類.

原文提到的__metaclass__屬性, 我思考了好久, 基本確認Ruby中沒有類似的概念. 就舉的將屬性名大寫的例子而言, 應該是在用class定義類時, 會自動調用這個屬性(所引用的函數對象). 初步看, 有種鉤子方法的感受. 就是"定義類"這個事件發生時, 會自動觸發執行__metaclass__屬性.

Ruby也有一些鉤子方法:

  • included表示模塊被包含時執行,

  • extended表示模塊被後擴展時執行,

  • prepended表示模塊被前擴展時執行,

  • inherited表示類被繼承時執行,

  • method_missing表示對象調用不存在的方法時執行.

但目前沒找到當定義類時被執行的鉤子方法. 因此像原文的大寫屬性名的操做, 還真不知道如何進行. 但事實上,Ruby的對應屬性的標識符有嚴格的規定, 不可能大寫首字母. 如類變量@@var, 實例變量@var, 方法名two_method.

但若是實現不了這個, 總以爲Ruby有種被比下去的感受, 雖然大寫全部屬性首字母的操做彷佛沒有意義.

class N
  def hello; "hello"; end
  instance_methods(false).each {|x| alias_method x.capitalize, x; remove_method x}
end
N.new.Hello  #=> "hello"
N.new.hello  #=> 方法未定義

這是大寫全部實例方法名的首字母, 核心的思想是, 爲原方法創建新的別名, 再刪掉原方法. 同Python同樣,Ruby的類是在執行代碼.

class N; puts "hello"; end  #=> "hello"

Ruby:

class N
  def self.world; "world"; end
  class << self
    instance_methods(false).each {|x| alias_method x.capitalize, x; remove_method x}
  end
end
N.World  #=> "world"
N.world  #=> 方法未定義

這是大寫全部的類方法名的首字母.

class N
  @name = "lzp"
  instance_variables.each {|x| instance_variable_set("@"+x.to_s[/\w+/].capitalize, @name); @name = nil}
end
N.class_eval {@Name}  #=> "lzp"
N.class_eval {@name}  #=> nil

這是大寫全部的類的實例變量.

因爲Ruby的實例變量默認是不能從外部訪問的, 不得不使用.class_eval來打開類的上下文.

不存在如何大寫全部實例變量的代碼, 所以在類實例化前, 實例對象的實例變量是不存在的.

好吧, 我認可, 這實現的很彆扭. 在同一操做的表述上, 不一樣語言有不一樣的書面寫法, 也天然有簡單有繁雜.

函數式特性

談點別的, 有關函數式特性, 使用map/filter/reduce.

Python:

a = ["he", "hk", "ok"]
list(map(lambda x: x*2, a))  #=>["hehe", "hkhk", "okok"]
list(filter(lambda x: x.startswith("h"), a))  #=> ["he", "hk"]
import functiontools.reduce
reduce(lambda x,y: x+":"+y, a)  #=> "he:hk:ok"

用上述函數來替換原文中的語句:

dict(map(lambda i: (i[0].upper(), i[1]), filter(lambda i: not i[0].startswith("__"), future_class_attr.items())))

好吧, 我認可個人Python技術真不高, 若是真寫成一行, 徹底看不懂了, 原文做者那樣寫更清晰簡潔易懂, 固然更主要的是, 用map/filter會引入新的難點, 容易偏離主題.

但願有高手能告訴我, 將一個類的全部非"__"的屬性的鍵變爲大寫如何以更函數式的方式表達出來.

Ruby:

a = ["he", "hk", "ok"]
a.map {|x| x*2}  #=> ["hehe", "hkhk", "okok"]
a.select {|x| x.start_with? "h"}  #=> ["he", "hk"]
a.reject {|x| x.start_with? "h"}  #=> ["ok"]
a.reduce {|sum, x| sum + ":" + x}  #=> "he:hk:ok"

一樣的, 用上述來替換原文的代碼.

future_class_attr.reject {|k,v| k.start_with? "__"}.map {|k,v| k.upcase}

Python3.x刪除了reduce函數, 推薦使用for循環, 也可使用funtools.reduce. 這跟Ruby徹底不一樣,Ruby提倡使用each, map等迭代, 而for在底層也是在調用each.

一切皆對象.

Python和Ruby都號稱一切皆對象, 但很明顯兩個的對象概念並不徹底對等.

Py: 1.__class__  #=> 語法錯誤
Py: a = 1; a.__class__  #=> int
Rb: 1.class  #=> Fixnum
Py: 1.real  #=> 語法錯誤
Py: b = 1; b.real  #=> 1
Rb: 1.real  #=> 1
Py: "lzp".upper()  #=>"LZP", 但在ipython中不補全方法
Py: s = "lzp"; s.upper()  #補全
Rb: "lzp".upcase  #=> "LZP", 補全

以上說明, 對對象和對象的引用調用方法是有區別的, 具體什麼原理以及詳細的區別, 我說明不了.

def hello(name): return "hello" + name
hello.__class__  #=> function

Ruby的方法不是對象, 不能賦值, 不能爲參傳遞.

def hello(name); "hello" + name; end
hello.class  #=> 參數錯誤
new_hello = hello  #=> 參數錯誤
def echo(o); o(); end
echo(hello)  #=> 參數錯誤

你是否是以爲問題挺大的, 這幾種對象的特徵居然都不知足. 但這些實際上是一個錯誤, 前文有提到, 對於Ruby的方法調用, 在不引發歧義的狀況下, ()是能夠省略. 在這裏, 全部出現hello的位置都默認你在調用方法, 但方法定義有參數, 你不傳遞參數, 因此錯誤是同一個, 少參數.

函數做爲對象最終用處都是被調用, 所以, 只從表面來看, Ruby中經過def定義的方法不是對象. 但本質上, 在Ruby中, 出現方法名的地方全被視爲對方法的調用, 也就是說, hello是方法調用, 而不是方法引用, 並不表徵方法自己. 那麼如何獲取方法自己的對象呢?

new_hello = method :hello
new_hello.call("lzp")  #=> "hellolzp"
new_hello.("lzp")  #=> "hellolzp"
new_hello["lzp"]  #=> "hellolzp"
new_hello.class  #=> Method
new_2_hello = new_hello

注意, 在這裏能夠看出, 在絕大部分語言中, ()都是函數調用的標誌. 但在Ruby中, ()只是在有歧義狀況下, 區分哪一個參數是哪一個函數的. 所以, 當函數做爲對象時, 不得不建立新的表示調用的標誌, 在這裏是.call, [], .().

函數並非惟一的可調用對象.

hello = lambda {|name| "hello" + name}
hello = ->(name) {"hello" + name}
hello = proc {|name| "hello" + name}
hello = Proc.new {|name| "hello" + name}

後記

事實上, Class.new 屬於 Ruby 元編程的一部分, 但 Ruby 的元編程就像普通編程同樣, 沒有任何神祕複雜的語法. 這裏真的只是冰山一角.

相關文章
相關標籤/搜索