ความแตกต่างของวิธีการเรียนใน Python: bound, unbound และ static


242

ความแตกต่างระหว่างวิธีการเรียนต่อไปนี้คืออะไร?

เป็นที่หนึ่งคงที่และอื่น ๆ ไม่ได้?

class Test(object):
  def method_one(self):
    print "Called method_one"

  def method_two():
    print "Called method_two"

a_test = Test()
a_test.method_one()
a_test.method_two()

18
ไม่มีความแตกต่างอื่น ๆ นอกเหนือจาก method_two () นิยามไม่ถูกต้องและการเรียกใช้ล้มเหลว
Anatoly techtonik

14
@ techtonik: ไม่มีอะไรผิดปกติกับความหมายของ method_two! มันถูกเรียกในสเป็คที่ไม่ถูกต้อง / ไม่ถูกต้องเช่นมีอาร์กิวเมนต์พิเศษ
0xc0de

1
คุณเป็นทั้งสองวิธีเช่นไม่ใช่วิธีการเรียน คุณสร้างวิธีการเรียนโดยใช้@classmethodกับคำจำกัดความ พารามิเตอร์แรกควรถูกเรียกclsใช้แทนselfและจะได้รับคลาสอ็อบเจ็กต์แทนที่จะเป็นอินสแตนซ์ของคลาสของคุณ: Test.method_three()และa_test.method_three()เทียบเท่า
Lutz Prechelt

ทำไมคุณต้องการสร้างนิยามฟังก์ชันโดยไม่มีselfอาร์กิวเมนต์? มีกรณีการใช้ที่แข็งแกร่งสำหรับเรื่องนี้หรือไม่?
alpha_989

คำตอบ:


412

ในหลามมีความแตกต่างระหว่างที่ถูกผูกไว้และไม่ได้ผูกไว้วิธีการ

โดยทั่วไปการเรียกฟังก์ชันสมาชิก (เช่นmethod_one) ฟังก์ชันที่ถูกผูกไว้

a_test.method_one()

แปลเป็น

Test.method_one(a_test)

นั่นคือการเรียกใช้เมธอด unbound ด้วยเหตุนี้การโทรไปยังเวอร์ชันของคุณจึงmethod_twoจะล้มเหลวด้วยTypeError

>>> a_test = Test() 
>>> a_test.method_two()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: method_two() takes no arguments (1 given) 

คุณสามารถเปลี่ยนพฤติกรรมของวิธีการโดยใช้มัณฑนากร

class Test(object):
    def method_one(self):
        print "Called method_one"

    @staticmethod
    def method_two():
        print "Called method two"

มัณฑนากรบอกในตัว metaclass เริ่มต้นtype(ชั้นชั้นที่ cf. คำถามนี้ ) method_twoต้องไม่สร้างวิธีการที่ถูกผูกไว้สำหรับ

ตอนนี้คุณสามารถเรียกใช้เมธอดสแตติกทั้งบนอินสแตนซ์หรือบนคลาสโดยตรง:

>>> a_test = Test()
>>> a_test.method_one()
Called method_one
>>> a_test.method_two()
Called method_two
>>> Test.method_two()
Called method_two

17
ฉันสนับสนุนคำตอบนี้มันยอดเยี่ยมกว่าของฉัน ทำได้ดี Torsten :)
FreeSpace

24
ใน python 3 วิธี unbound ถูกคัดค้าน แทนที่จะมีเพียงฟังก์ชั่น
boldnik

@boldnik ทำไมคุณถึงบอกว่าวิธีที่ไม่ได้ผูกไว้เลิกใช้แล้ว? วิธีการสแตติกยังคงมีอยู่ในเอกสาร: docs.python.org/3/library/functions.html#staticmethod
alpha_989

195

วิธีการใน Python เป็นสิ่งที่ง่ายมาก ๆ เมื่อคุณเข้าใจพื้นฐานของระบบ descriptor ลองนึกภาพชั้นเรียนต่อไปนี้:

class C(object):
    def foo(self):
        pass

ตอนนี้เรามาดูคลาสนั้นในเชลล์:

>>> C.foo
<unbound method C.foo>
>>> C.__dict__['foo']
<function foo at 0x17d05b0>

ในขณะที่คุณสามารถดูว่าคุณเข้าถึงfooคุณลักษณะในชั้นเรียนที่คุณได้รับกลับวิธีการที่ไม่ถูกผูกไว้ แต่ภายในหน่วยเก็บข้อมูลคลาส (dict) มีฟังก์ชั่น ทำไมเป็นอย่างนั้น? เหตุผลสำหรับสิ่งนี้คือคลาสของคลาสของคุณใช้ a __getattribute__ที่แก้ไข descriptors ฟังดูซับซ้อน แต่ไม่ใช่ C.fooใกล้เคียงกับรหัสนี้ในกรณีพิเศษ:

>>> C.__dict__['foo'].__get__(None, C)
<unbound method C.foo>

นั่นเป็นเพราะฟังก์ชั่นมี__get__วิธีการที่ทำให้พวกเขาอธิบาย หากคุณมีตัวอย่างของคลาสมันเกือบจะเหมือนกันนั่นNoneคือตัวอย่างของคลาส:

>>> c = C()
>>> C.__dict__['foo'].__get__(c, C)
<bound method C.foo of <__main__.C object at 0x17bd4d0>>

ทำไมงูหลามถึงทำเช่นนั้น? เพราะวัตถุวิธีการผูกพารามิเตอร์ตัวแรกของฟังก์ชั่นกับอินสแตนซ์ของชั้นเรียน นั่นคือสิ่งที่ตัวเองมาจาก ตอนนี้บางครั้งคุณไม่ต้องการให้คลาสของคุณสร้างฟังก์ชั่นวิธีการนั่นคือที่staticmethodมา:

 class C(object):
  @staticmethod
  def foo():
   pass

staticmethodมัณฑนากร wraps ชั้นเรียนและการดำเนินการของคุณหุ่น__get__ที่ส่งกลับฟังก์ชันห่อเป็นฟังก์ชั่นและไม่ได้เป็นวิธีการ:

>>> C.__dict__['foo'].__get__(None, C)
<function foo at 0x17d0c30>

หวังว่าจะอธิบายมัน


12
staticmethodมัณฑนากร wraps ชั้นเรียนของคุณ ( ... )วลีนี้เป็นบิตทำให้เข้าใจผิดเป็นชั้นที่จะถูกห่อหุ้มเป็นชั้นของวิธีการfooและไม่ได้ระดับที่fooถูกกำหนดไว้
Piotr Dobrogost

12

เมื่อคุณเรียกสมาชิกคลาส Python จะใช้การอ้างอิงไปยังวัตถุเป็นพารามิเตอร์แรกโดยอัตโนมัติ ตัวแปรไม่selfได้มีความหมายอะไรเลยมันเป็นแค่การเขียนโค้ด คุณสามารถเรียกมันได้gargalooถ้าต้องการ ดังที่กล่าวไว้การเรียกไปสู่การmethod_twoเพิ่ม a TypeErrorเนื่องจาก Python พยายามส่งพารามิเตอร์โดยอัตโนมัติ (การอ้างอิงไปยังวัตถุพาเรนต์) ไปยังวิธีที่กำหนดไว้ว่าไม่มีพารามิเตอร์

เพื่อให้มันใช้งานได้จริงคุณสามารถต่อท้ายคำจำกัดความของคลาส:

method_two = staticmethod(method_two)

หรือคุณอาจจะใช้มัณฑนากรฟังก์ชั่น@staticmethod


4
คุณหมายถึง "ไวยากรณ์ของมัณฑนากร @staticmethod ฟังก์ชัน"
tzot

11
>>> class Class(object):
...     def __init__(self):
...         self.i = 0
...     def instance_method(self):
...         self.i += 1
...         print self.i
...     c = 0
...     @classmethod
...     def class_method(cls):
...         cls.c += 1
...         print cls.c
...     @staticmethod
...     def static_method(s):
...         s += 1
...         print s
... 
>>> a = Class()
>>> a.class_method()
1
>>> Class.class_method()    # The class shares this value across instances
2
>>> a.instance_method()
1
>>> Class.instance_method() # The class cannot use an instance method
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unbound method instance_method() must be called with Class instance as first argument (got nothing instead)
>>> Class.instance_method(a)
2
>>> b = 0
>>> a.static_method(b)
1
>>> a.static_method(a.c) # Static method does not have direct access to 
>>>                      # class or instance properties.
3
>>> Class.c        # a.c above was passed by value and not by reference.
2
>>> a.c
2
>>> a.c = 5        # The connection between the instance
>>> Class.c        # and its class is weak as seen here.
2
>>> Class.class_method()
3
>>> a.c
5

2
Class.instance_method() # The class cannot use an instance methodมันสามารถใช้ เพียงส่งตัวอย่างด้วยตนเอง:Class.instance_method(a)
warvariuc

@warwaruk ที่นั่นให้ดูที่บรรทัดด้านล่างTyeErrorบรรทัด
kzh

ใช่ฉันเห็นมันในภายหลัง ถึงกระนั้นก็ไม่ถูกต้องที่จะพูดว่า 'ชั้นไม่สามารถใช้วิธีการอินสแตนซ์' ได้เพราะคุณทำเพียงหนึ่งบรรทัดด้านล่าง
warvariuc

@ kzh ขอบคุณสำหรับคำอธิบายของคุณ เมื่อคุณเรียกว่าa.class_method()ดูเหมือนว่าa.cมีการปรับปรุงเพื่อให้1เพื่อให้การโทรClass.class_method(), ปรับปรุงตัวแปรClass.c 2แต่เมื่อคุณได้รับมอบหมายa.c=5ทำไมClass.cไม่ได้รับการปรับปรุงเพื่อ5?
alpha_989

@ alpha_989 python จะค้นหาแอททริบิวโดยตรงจากอินสแตนซ์ของตัวเองก่อนและหากไม่ได้อยู่ที่นั่นแอปนั้นจะค้นหาแอปนั้นในคลาสด้วยค่าเริ่มต้น หากคุณมีคำถามอื่นใดเกี่ยวกับเรื่องนี้โปรดเปิดคำถามและเชื่อมโยงที่นี่และฉันยินดีที่จะช่วยเหลือเพิ่มเติม
kzh

4

method_two ไม่ทำงานเพราะคุณกำลังกำหนดฟังก์ชั่นสมาชิก แต่ไม่ได้บอกว่าฟังก์ชั่นนี้เป็นสมาชิกของอะไร หากคุณดำเนินการบรรทัดสุดท้ายคุณจะได้รับ:

>>> a_test.method_two()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: method_two() takes no arguments (1 given)

หากคุณกำหนดฟังก์ชั่นสมาชิกสำหรับคลาสอาร์กิวเมนต์แรกจะต้องเป็น 'ตัวเอง' เสมอ


3

คำอธิบายที่ถูกต้องจาก Armin Ronacher ด้านบนขยายคำตอบของเขาเพื่อให้ผู้เริ่มต้นอย่างฉันเข้าใจดี:

ความแตกต่างในวิธีการที่กำหนดไว้ในคลาสไม่ว่าจะเป็นวิธีการคงที่หรืออินสแตนซ์ (ยังมีวิธีการเรียนแบบอื่น - ไม่ได้กล่าวถึงที่นี่เพื่อข้ามมัน) วางในความเป็นจริงว่าพวกเขาจะผูกพันกับอินสแตนซ์ชั้นหรือไม่ ตัวอย่างเช่นพูดว่าวิธีการได้รับการอ้างอิงถึงอินสแตนซ์ของชั้นเรียนในช่วงรันไทม์

class C:
    a = [] 
    def foo(self):
        pass

C # this is the class object
C.a # is a list object (class property object)
C.foo # is a function object (class property object)
c = C() 
c # this is the class instance

__dict__คุณสมบัติในพจนานุกรมของวัตถุชั้นถืออ้างอิงถึงคุณสมบัติและวิธีการของวัตถุชั้นและทำให้

>>> C.__dict__['foo']
<function foo at 0x17d05b0>

วิธีการ foo สามารถเข้าถึงได้ดังกล่าวข้างต้น จุดสำคัญที่ควรทราบที่นี่คือทุกอย่างในหลามเป็นวัตถุดังนั้นการอ้างอิงในพจนานุกรมด้านบนจึงเป็นตัวชี้ไปยังวัตถุอื่น ให้ฉันเรียกพวกเขาว่า Class Property Objects - หรือเป็น CPO ภายในขอบเขตของคำตอบของฉันเพื่อความกระชับ

หาก CPO เป็นตัวบ่งชี้ตัวแปลภาษาไพ ธ อนจะเรียก__get__()เมธอดของ CPO เพื่อเข้าถึงค่าที่มีอยู่

เพื่อตรวจสอบว่า CPO เป็น descriptor หรือไม่งูเหลือมล่ามตรวจสอบว่ามันใช้โปรโตคอล descriptor หรือไม่ ในการนำโพรโทคอล descriptor ไปใช้นั้นมี 3 วิธี

def __get__(self, instance, owner)
def __set__(self, instance, value)
def __delete__(self, instance)

สำหรับเช่น

>>> C.__dict__['foo'].__get__(c, C)

ที่ไหน

  • self คือ CPO (อาจเป็นตัวอย่างของรายการ str ฟังก์ชั่น ฯลฯ ) และจัดทำโดยรันไทม์
  • instance เป็นตัวอย่างของคลาสที่มีการกำหนด CPO นี้ (วัตถุ 'c' ด้านบน) และจะต้องมีการอธิบายโดยเรา
  • ownerเป็นคลาสที่กำหนด CPO นี้ (คลาสอ็อบเจ็กต์ 'C' ด้านบน) และจำเป็นต้องจัดหาโดยเรา อย่างไรก็ตามนี่เป็นเพราะเรากำลังเรียกมันบน CPO เมื่อเราเรียกมันว่าบนอินสแตนซ์เราไม่จำเป็นต้องให้สิ่งนี้เนื่องจากรันไทม์สามารถจัดหาอินสแตนซ์หรือคลาสของมัน (polymorphism)
  • value คือคุณค่าที่ตั้งใจไว้สำหรับ CPO และเราต้องจัดหาให้

CPO ไม่ใช่ทุกคนที่เป็นตัวอธิบาย ตัวอย่างเช่น

>>> C.__dict__['foo'].__get__(None, C)
<function C.foo at 0x10a72f510> 
>>> C.__dict__['a'].__get__(None, C)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'list' object has no attribute '__get__'

นี่เป็นเพราะคลาสลิสต์ไม่ได้ใช้โปรโตคอล descriptor

ดังนั้นจึงc.foo(self)จำเป็นต้องมีการโต้แย้งตัวเองเพราะลายเซ็นของวิธีการนี้เป็นจริงC.__dict__['foo'].__get__(c, C)(ตามที่อธิบายไว้ข้างต้น C ไม่จำเป็นเนื่องจากสามารถพบได้หรือ polymorphed) และนี่คือสาเหตุที่คุณได้รับ TypeError ถ้าคุณไม่ผ่านอาร์กิวเมนต์อินสแตนซ์ที่ต้องการ

หากคุณสังเกตเห็นว่าวิธีการนั้นยังคงถูกอ้างอิงผ่านคลาส Object C และการเชื่อมโยงกับอินสแตนซ์ของคลาสทำได้โดยผ่านบริบทในรูปแบบของวัตถุอินสแตนซ์ในฟังก์ชันนี้

สิ่งนี้ยอดเยี่ยมมากเพราะถ้าคุณเลือกที่จะไม่เก็บบริบทหรือไม่ผูกมัดกับอินสแตนซ์ทั้งหมดที่จำเป็นต้องมีคือการเขียนคลาสเพื่อตัดคำอธิบาย CPO และแทนที่__get__()เมธอดของมันเพื่อไม่ต้องใช้บริบท คลาสใหม่นี้คือสิ่งที่เราเรียกว่ามัณฑนากรและใช้ผ่านคำสำคัญ@staticmethod

class C(object):
  @staticmethod
  def foo():
   pass

การไม่มีบริบทใน CPO ที่ปิดใหม่fooไม่ได้เกิดข้อผิดพลาดและสามารถตรวจสอบได้ดังนี้:

>>> C.__dict__['foo'].__get__(None, C)
<function foo at 0x17d0c30>

กรณีการใช้งานของวิธีการแบบคงที่เป็นมากกว่าหนึ่ง namespacing และการบำรุงรักษารหัสหนึ่ง (นำออกจากชั้นเรียนและทำให้มันใช้ได้ตลอดโมดูล ฯลฯ )

มันอาจจะดีกว่าที่จะเขียนวิธีการคงที่แทนที่จะเป็นวิธีการอินสแตนซ์ที่เป็นไปได้เว้นแต่ ofcourse คุณต้อง contexualise วิธีการ (เช่นการเข้าถึงตัวแปรอินสแตนซ์ตัวแปรคลาส ฯลฯ ) เหตุผลหนึ่งคือทำให้การเก็บขยะง่ายขึ้นโดยไม่เก็บการอ้างอิงวัตถุ


1

นั่นคือข้อผิดพลาด

ก่อนอื่นบรรทัดแรกควรเป็นเช่นนี้ (ระวังเมืองหลวง)

class Test(object):

เมื่อใดก็ตามที่คุณเรียกเมธอดของคลาสมันจะได้รับเป็นอาร์กิวเมนต์แรก (ดังนั้นชื่อตัวเอง) และ method_two ให้ข้อผิดพลาดนี้

>>> a.method_two()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: method_two() takes no arguments (1 given)

1

อันที่สองไม่ทำงานเพราะเมื่อคุณเรียกมันว่าไพ ธ อนพยายามเรียกมันด้วยอินสแตนซ์ a_test เป็นอาร์กิวเมนต์แรก แต่ method_two ของคุณไม่ยอมรับอาร์กิวเมนต์ใด ๆ ดังนั้นมันจะไม่ทำงานคุณจะได้รับรันไทม์ ความผิดพลาด หากคุณต้องการเทียบเท่าวิธีคงที่คุณสามารถใช้วิธีการเรียน มีความต้องการน้อยมากสำหรับวิธีการเรียนใน Python กว่าวิธีการคงที่ในภาษาเช่น Java หรือ C # ส่วนใหญ่ทางออกที่ดีที่สุดคือการใช้วิธีการในโมดูลนอกคำจำกัดความของคลาสนั้นทำงานได้อย่างมีประสิทธิภาพมากกว่าวิธีการเรียน


หากฉันกำหนดฟังก์ชั่น Class Test(object): @staticmethod def method_two(): print(“called method_two”) One use case ฉันคิดว่าเมื่อคุณต้องการให้ฟังก์ชันเป็นส่วนหนึ่งของคลาส แต่ไม่ต้องการให้ผู้ใช้เข้าถึงฟังก์ชันโดยตรง ดังนั้นmethod_twoสามารถเรียกใช้โดยฟังก์ชั่นอื่น ๆ ภายในTestอินสแตนซ์ แต่ไม่สามารถเรียกa_test.method_two()ใช้ได้ ถ้าฉันใช้def method_two()มันจะใช้ได้กับกรณีการใช้งานนี้หรือไม่? หรือมีวิธีที่ดีกว่าในการแก้ไขคำจำกัดความของฟังก์ชั่นดังนั้นจึงใช้งานได้ตามวัตถุประสงค์สำหรับกรณีการใช้งานข้างต้น?
alpha_989

1

การเรียกเมธอด _ สองจะทำให้เกิดข้อยกเว้นสำหรับการไม่ยอมรับพารามิเตอร์ตัวเองรันไทม์ Python จะผ่านมันไปโดยอัตโนมัติ

staticmethod decoratorหากคุณต้องการที่จะสร้างวิธีการคงในระดับงูใหญ่ตกแต่งด้วย

Class Test(Object):
  @staticmethod
  def method_two():
    print "Called method_two"

Test.method_two()


0

คำจำกัดความของmethod_twoไม่ถูกต้อง เมื่อคุณโทรmethod_twoคุณจะได้รับTypeError: method_two() takes 0 positional arguments but 1 was givenจากล่าม

วิธีการเช่นเป็นฟังก์ชั่น จำกัด a_test.method_two()เมื่อคุณเรียกมันเหมือน มันยอมรับโดยอัตโนมัติselfซึ่งชี้ไปที่อินสแตนซ์ของTestเป็นพารามิเตอร์แรก ผ่านselfพารามิเตอร์วิธีการอินสแตนซ์สามารถเข้าถึงคุณลักษณะได้อย่างอิสระและปรับเปลี่ยนพวกเขาในวัตถุเดียวกัน


0

วิธีการไม่ถูกผูกไว้

เมธอด Unbound เป็นเมธอดที่ไม่ได้ผูกกับอินสแตนซ์ของคลาสที่เฉพาะเจาะจง

วิธีการที่ถูกผูกไว้

วิธีการที่ถูกผูกไว้เป็นวิธีที่ถูกผูกไว้กับอินสแตนซ์ที่เฉพาะเจาะจงของชั้นเรียน

ดังที่มีการบันทึกไว้ที่นี่ตนเองสามารถอ้างถึงสิ่งต่าง ๆ ได้ขึ้นอยู่กับฟังก์ชันที่ถูกผูกไว้ไม่ถูกผูกมัดหรือคงที่

ลองดูตัวอย่างต่อไปนี้:

class MyClass:    
    def some_method(self):
        return self  # For the sake of the example

>>> MyClass().some_method()
<__main__.MyClass object at 0x10e8e43a0># This can also be written as:>>> obj = MyClass()

>>> obj.some_method()
<__main__.MyClass object at 0x10ea12bb0>

# Bound method call:
>>> obj.some_method(10)
TypeError: some_method() takes 1 positional argument but 2 were given

# WHY IT DIDN'T WORK?
# obj.some_method(10) bound call translated as
# MyClass.some_method(obj, 10) unbound method and it takes 2 
# arguments now instead of 1 

# ----- USING THE UNBOUND METHOD ------
>>> MyClass.some_method(10)
10

เนื่องจากเราไม่ได้ใช้อินสแตนซ์ของคลาส - obj- ในการโทรล่าสุดเราสามารถบอกได้ว่ามันดูเหมือนว่าเป็นวิธีการคงที่

ถ้าเป็นเช่นนั้นอะไรคือความแตกต่างระหว่างการMyClass.some_method(10)โทรและการโทรไปยังฟังก์ชั่นคงที่ตกแต่งด้วย@staticmethodมัณฑนากร?

โดยการใช้มัณฑนากรเราทำให้ชัดเจนว่าจะใช้วิธีนี้โดยไม่ต้องสร้างอินสแตนซ์สำหรับมันก่อน โดยปกติจะไม่คาดหวังว่าวิธีการสมาชิกคลาสจะใช้โดยไม่มีอินสแตนซ์และ accesing พวกเขาสามารถทำให้เกิดข้อผิดพลาดที่เป็นไปได้ขึ้นอยู่กับโครงสร้างของวิธีการ

นอกจากนี้ด้วยการเพิ่มผู้@staticmethodตกแต่งเราทำให้สามารถเข้าถึงวัตถุได้เช่นกัน

class MyClass:    
    def some_method(self):
        return self    

    @staticmethod
    def some_static_method(number):
        return number

>>> MyClass.some_static_method(10)   # without an instance
10
>>> MyClass().some_static_method(10)   # Calling through an instance
10

คุณไม่สามารถทำตัวอย่างข้างต้นด้วยวิธีการอินสแตนซ์ คุณสามารถเอาชีวิตรอดคนแรก (อย่างที่เราเคยทำมาก่อน) แต่อันที่สองจะถูกแปลเป็นสายที่ไม่ได้ผูกMyClass.some_method(obj, 10)ซึ่งจะยกระดับTypeErrorเนื่องจากวิธีอินสแตนซ์รับอาร์กิวเมนต์หนึ่งและคุณพยายามผ่านสองโดยไม่ได้ตั้งใจ

จากนั้นคุณอาจพูดว่า“ ถ้าฉันสามารถเรียกวิธีการคงที่ผ่านทั้งอินสแตนซ์และคลาสMyClass.some_static_methodและMyClass().some_static_methodควรเป็นวิธีเดียวกัน” ใช่


0

วิธีการที่ถูกผูกไว้ = วิธีการเช่น

Unbound method = วิธีการคงที่


โปรดเพิ่มคำอธิบายเพิ่มเติมลงในคำตอบของคุณเพื่อให้คนอื่นสามารถเรียนรู้ได้ ตัวอย่างเช่นลองดูคำตอบอื่น ๆ ของคำถามนี้
Nico Haase
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.