pythonic เป็นวิธีการใช้ getters และ setters อะไร


341

ฉันทำมันเหมือน:

def set_property(property,value):  
def get_property(property):  

หรือ

object.property = value  
value = object.property

ฉันใหม่กับ Python ดังนั้นฉันยังคงสำรวจไวยากรณ์อยู่และฉันต้องการคำแนะนำในการทำสิ่งนี้

คำตอบ:


684

ลองสิ่งนี้: คุณสมบัติของ Python

รหัสตัวอย่างคือ:

class C(object):
    def __init__(self):
        self._x = None

    @property
    def x(self):
        """I'm the 'x' property."""
        print("getter of x called")
        return self._x

    @x.setter
    def x(self, value):
        print("setter of x called")
        self._x = value

    @x.deleter
    def x(self):
        print("deleter of x called")
        del self._x


c = C()
c.x = 'foo'  # setter called
foo = c.x    # getter called
del c.x      # deleter called

2
setter สำหรับ x ถูกเรียกใน initializer เมื่อสร้างอินสแตนซ์ _x หรือไม่?
Casey

7
@Casey: ไม่การอ้างอิงถึง._x(ซึ่งไม่ใช่สถานที่ให้บริการเพียงแอตทริบิวต์ธรรมดา) หลีกเลี่ยงการpropertyตัดคำ เฉพาะการอ้างอิงถึงผ่านไป.x property
ShadowRanger

278

pythonic เป็นวิธีการใช้ getters และ setters อะไร

วิธี "Pythonic" ไม่ใช้ "getters" และ "setters" แต่ใช้แอตทริบิวต์แบบธรรมดาเช่นคำถามแสดงให้เห็นและdelสำหรับการลบ (แต่ชื่อจะเปลี่ยนเพื่อปกป้องผู้บริสุทธิ์ ... builtins):

value = 'something'

obj.attribute = value  
value = obj.attribute
del obj.attribute

หากภายหลังคุณต้องการแก้ไขการตั้งค่าและรับคุณสามารถทำได้โดยไม่ต้องเปลี่ยนรหัสผู้ใช้โดยใช้เครื่องมือpropertyตกแต่ง:

class Obj:
    """property demo"""
    #
    @property            # first decorate the getter method
    def attribute(self): # This getter method name is *the* name
        return self._attribute
    #
    @attribute.setter    # the property decorates with `.setter` now
    def attribute(self, value):   # name, e.g. "attribute", is the same
        self._attribute = value   # the "value" name isn't special
    #
    @attribute.deleter     # decorate with `.deleter`
    def attribute(self):   # again, the method name is the same
        del self._attribute

(สำเนาการใช้มัณฑนากรแต่ละคนและอัปเดตวัตถุคุณสมบัติก่อนหน้าดังนั้นโปรดทราบว่าคุณควรใช้ชื่อเดียวกันสำหรับแต่ละชุดรับและลบฟังก์ชัน / เมธอด

หลังจากกำหนดข้างต้นการตั้งค่าการรับและการลบรหัสเดิมจะเหมือนกัน:

obj = Obj()
obj.attribute = value  
the_value = obj.attribute
del obj.attribute

คุณควรหลีกเลี่ยงสิ่งนี้:

def set_property(property,value):  
def get_property(property):  

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

class Obj:

    def set_property(self, property, value): # don't do this
        ...
    def get_property(self, property):        # don't do this either
        ...

ประการที่สองนี่เป็นการทำซ้ำวัตถุประสงค์ของสองวิธีพิเศษ__setattr__และ__getattr__และ

ประการที่สามเรายังมีฟังก์ชั่นsetattrและgetattrbuiltin

setattr(object, 'property_name', value)
getattr(object, 'property_name', default_value)  # default is optional

@propertyมัณฑนากรสำหรับ getters สร้างและ setters

ตัวอย่างเช่นเราสามารถปรับเปลี่ยนพฤติกรรมการตั้งค่าเพื่อ จำกัด การตั้งค่าไว้:

class Protective(object):

    @property
    def protected_value(self):
        return self._protected_value

    @protected_value.setter
    def protected_value(self, value):
        if acceptable(value): # e.g. type or range check
            self._protected_value = value

โดยทั่วไปเราต้องการหลีกเลี่ยงการใช้propertyและเพียงแค่ใช้คุณสมบัติโดยตรง

นี่คือสิ่งที่ผู้ใช้ Python คาดหวัง การปฏิบัติตามกฎของความประหลาดใจน้อยที่สุดคุณควรพยายามให้สิ่งที่พวกเขาคาดหวังแก่ผู้ใช้ของคุณเว้นแต่คุณจะมีเหตุผลที่น่าสนใจอย่างมาก

สาธิต

ตัวอย่างเช่นสมมติว่าเราต้องการแอตทริบิวต์ที่มีการป้องกันของวัตถุเป็นจำนวนเต็มระหว่าง 0 ถึง 100 และป้องกันการลบโดยมีข้อความที่เหมาะสมเพื่อแจ้งให้ผู้ใช้ทราบถึงการใช้งานที่เหมาะสม:

class Protective(object):
    """protected property demo"""
    #
    def __init__(self, start_protected_value=0):
        self.protected_value = start_protected_value
    # 
    @property
    def protected_value(self):
        return self._protected_value
    #
    @protected_value.setter
    def protected_value(self, value):
        if value != int(value):
            raise TypeError("protected_value must be an integer")
        if 0 <= value <= 100:
            self._protected_value = int(value)
        else:
            raise ValueError("protected_value must be " +
                             "between 0 and 100 inclusive")
    #
    @protected_value.deleter
    def protected_value(self):
        raise AttributeError("do not delete, protected_value can be set to 0")

(โปรดทราบว่า__init__หมายถึงself.protected_valueแต่อ้างถึงวิธีการของคุณสมบัติself._protected_valueนี่เป็นการ__init__ใช้คุณสมบัติผ่าน API สาธารณะเพื่อให้มั่นใจว่าเป็น "ป้องกัน")

และการใช้งาน:

>>> p1 = Protective(3)
>>> p1.protected_value
3
>>> p1 = Protective(5.0)
>>> p1.protected_value
5
>>> p2 = Protective(-5)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 3, in __init__
  File "<stdin>", line 15, in protected_value
ValueError: protectected_value must be between 0 and 100 inclusive
>>> p1.protected_value = 7.3
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 17, in protected_value
TypeError: protected_value must be an integer
>>> p1.protected_value = 101
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 15, in protected_value
ValueError: protectected_value must be between 0 and 100 inclusive
>>> del p1.protected_value
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 18, in protected_value
AttributeError: do not delete, protected_value can be set to 0

ชื่อมีความสำคัญหรือไม่

ใช่พวกเขาทำ .setterและ.deleterทำสำเนาของคุณสมบัติดั้งเดิม สิ่งนี้ทำให้คลาสย่อยสามารถปรับเปลี่ยนพฤติกรรมได้อย่างเหมาะสมโดยไม่ต้องปรับเปลี่ยนพฤติกรรมในพาเรนต์

class Obj:
    """property demo"""
    #
    @property
    def get_only(self):
        return self._attribute
    #
    @get_only.setter
    def get_or_set(self, value):
        self._attribute = value
    #
    @get_or_set.deleter
    def get_set_or_delete(self):
        del self._attribute

ตอนนี้เพื่อให้ใช้งานได้คุณต้องใช้ชื่อที่เกี่ยวข้อง:

obj = Obj()
# obj.get_only = 'value' # would error
obj.get_or_set = 'value'  
obj.get_set_or_delete = 'new value'
the_value = obj.get_only
del obj.get_set_or_delete
# del obj.get_or_set # would error

ฉันไม่แน่ใจว่าสิ่งนี้จะมีประโยชน์ แต่กรณีใช้คือถ้าคุณต้องการได้รับการตั้งค่าและ / หรือคุณสมบัติลบเท่านั้น อาจดีที่สุดที่จะติดกับคุณสมบัติเดียวกันความหมายมีชื่อเดียวกัน

ข้อสรุป

เริ่มต้นด้วยคุณสมบัติง่าย ๆ

หากคุณต้องการฟังก์ชันการทำงานในภายหลังเกี่ยวกับการตั้งค่าการรับและการลบคุณสามารถเพิ่มได้ด้วยตัวตกแต่งคุณสมบัติ

หลีกเลี่ยงการใช้ชื่อฟังก์ชั่นset_...และget_...- นั่นคือคุณสมบัติที่มีไว้สำหรับ


นอกเหนือจากการทำซ้ำฟังก์ชันการทำงานที่มีอยู่แล้วเหตุใดจึงควรหลีกเลี่ยงการเขียน setters และ getters ของคุณเอง? ฉันเข้าใจว่าอาจไม่ใช่วิธี Pythonic แต่มีปัญหาที่ร้ายแรงอย่างใดอย่างหนึ่งที่อาจพบเป็นอย่างอื่นได้หรือไม่
user1350191

4
ในการสาธิตของคุณ__init__วิธีหมายถึงself.protected_valueแต่ทะเยอทะยานและ setters self._protected_valueอ้างถึง คุณช่วยอธิบายการทำงานของมันได้มั้ย ฉันทดสอบโค้ดของคุณและใช้งานได้ตามปกติ - นี่ไม่ใช่การพิมพ์ผิด
codeforester

2
@ codeforester ฉันหวังว่าจะตอบในคำตอบก่อนหน้านี้ แต่จนกว่าฉันจะทำได้ความคิดเห็นนี้ควรจะเพียงพอ ฉันหวังว่าคุณจะเห็นว่ามันใช้ทรัพย์สินผ่าน API สาธารณะทำให้มั่นใจได้ว่า "ได้รับการป้องกัน" มันจะไม่ทำให้รู้สึก "ปกป้อง" กับคุณสมบัติและจากนั้นใช้ API ที่ไม่ใช่แบบสาธารณะแทนใน__init__มันจะ?
Aaron Hall

2
ใช่ @AaronHall รับทันที ฉันไม่รู้ว่าself.protected_value = start_protected_valueจริง ๆ แล้วกำลังเรียกฟังก์ชัน setter ฉันคิดว่ามันเป็นงานที่มอบหมาย
codeforester

1
imho สิ่งนี้ควรเป็นคำตอบที่ยอมรับได้ถ้าฉันเข้าใจว่างูใหญ่ถูกต้องใช้เวลาเพียงจุดตรงข้ามเมื่อเทียบกับจาวาเช่น แทนที่จะทำให้ทุกอย่างเป็นส่วนตัวโดยค่าเริ่มต้นและเขียนรหัสพิเศษบางอย่างเมื่อจำเป็นต่อสาธารณะในไพ ธ อนคุณสามารถทำให้ทุกอย่างเป็นสาธารณะและเพิ่มความเป็นส่วนตัวได้ในภายหลัง
idclev 463035818

27
In [1]: class test(object):
    def __init__(self):
        self.pants = 'pants'
    @property
    def p(self):
        return self.pants
    @p.setter
    def p(self, value):
        self.pants = value * 2
   ....: 
In [2]: t = test()
In [3]: t.p
Out[3]: 'pants'
In [4]: t.p = 10
In [5]: t.p
Out[5]: 20

17

การใช้@propertyและ@attribute.setterช่วยให้คุณไม่เพียง แต่ใช้วิธี "pythonic" แต่ยังตรวจสอบความถูกต้องของคุณลักษณะทั้งในขณะที่สร้างวัตถุและเมื่อทำการเปลี่ยนแปลง

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

    @property
    def name(self):
        return self._name

    @name.setter
    def name(self, new_name):
        if type(new_name) == str: #type checking for name property
            self._name = new_name
        else:
            raise Exception("Invalid value for name")

โดยสิ่งนี้คุณ 'ซ่อน' _nameแอตทริบิวต์จากนักพัฒนาไคลเอนต์และทำการตรวจสอบกับชนิดคุณสมบัติชื่อ โปรดทราบว่าโดยทำตามวิธีนี้แม้ในระหว่างการเริ่มต้น setter จะถูกเรียก ดังนั้น:

p = Person(12)

จะนำไปสู่:

Exception: Invalid value for name

แต่:

>>>p = person('Mike')
>>>print(p.name)
Mike
>>>p.name = 'George'
>>>print(p.name)
George
>>>p.name = 2.3 # Causes an exception

16

ตรวจสอบมัณฑนากร@property


34
นี่เป็นคำตอบสำหรับลิงค์เท่านั้น
Aaron Hall


7
นี่เป็นคำตอบที่สมบูรณ์ได้อย่างไร ลิงค์ไม่ใช่คำตอบ
Andy_A̷n̷d̷y̷

ฉันคิดว่ามันเป็นคำตอบที่ดีเพราะเอกสารระบุอย่างชัดเจนว่าจะใช้อย่างไร (และจะยังคงเป็นปัจจุบันหากการเปลี่ยนแปลงของ python มีผลและนำ OP ไปสู่วิธีการที่คำตอบแนะนำไว้ @ Jean-FrançoisCorbettระบุไว้อย่างชัดเจน 'อย่างไร' เป็นคำตอบที่สมบูรณ์
HunnyBear

ไม่ว่าในกรณีใดคำตอบนี้จะไม่เพิ่มสิ่งใดที่เป็นประโยชน์ต่อคำตอบอื่น ๆ และควรลบทิ้ง
Georgy

5

คุณสามารถใช้ accessors / mutators (เช่น@attr.setterและ@property) หรือไม่ แต่สิ่งที่สำคัญที่สุดคือให้สอดคล้อง!

หากคุณใช้@propertyเพื่อเข้าถึงแอตทริบิวต์เช่น

class myClass:
    def __init__(a):
        self._a = a

    @property
    def a(self):
        return self._a

ใช้เพื่อเข้าถึงทุก ๆคุณสมบัติ* ! มันจะเป็นการปฏิบัติที่ไม่ดีในการเข้าถึงคุณลักษณะบางอย่างโดยใช้@propertyและปล่อยให้คุณสมบัติอื่น ๆสาธารณะ (เช่นชื่อที่ไม่มีเครื่องหมายขีดเส้นใต้) โดยไม่มีการเข้าถึงเช่นไม่ทำ

class myClass:
    def __init__(a, b):
        self.a = a
        self.b = b

    @property
    def a(self):
        return self.a

โปรดทราบว่าself.bไม่มีการเข้าถึงอย่างชัดเจนที่นี่แม้ว่าจะเป็นสาธารณะ

ในทำนองเดียวกันกับsetters (หรือmutators ) อย่าลังเลที่จะใช้@attribute.setterแต่ต้องสอดคล้อง! เมื่อคุณทำเช่น

class myClass:
    def __init__(a, b):
        self.a = a
        self.b = b 

    @a.setter
    def a(self, value):
        return self.a = value

มันยากสำหรับฉันที่จะคาดเดาความตั้งใจของคุณ ในมือข้างหนึ่งคุณกำลังบอกว่าทั้งสองaและbเป็นสาธารณะ (ไม่มีขีดเส้นใต้นำในชื่อของพวกเขา) ดังนั้นฉันควรได้รับอนุญาตตามหลักวิชาในการเข้าถึง / กลายพันธุ์ (รับ / ชุด) ทั้งสอง แต่จากนั้นคุณระบุตัวแปลที่ชัดเจนเท่านั้นสำหรับaซึ่งบอกฉันว่าบางทีฉันไม่สามารถตั้งค่าbได้ เนื่องจากคุณได้ระบุ Mutator อย่างชัดเจนฉันไม่แน่ใจว่าการขาด accessor ( @property) อย่างชัดเจนหมายความว่าฉันไม่ควรเข้าถึงตัวแปรเหล่านั้นหรือคุณเพียงแค่ประหยัดในการใช้@propertyหมายความว่าฉันไม่ควรจะสามารถเข้าถึงอย่างใดอย่างหนึ่งของตัวแปรเหล่านั้นหรือคุณแค่ประหยัดในการใช้

* ข้อยกเว้นคือเมื่อคุณต้องการทำให้ตัวแปรบางตัวสามารถเข้าถึงได้หรือไม่แน่นอนแต่ไม่ใช่ทั้งสองอย่างหรือคุณต้องการดำเนินการตรรกะเพิ่มเติมเมื่อเข้าถึงหรือกลายพันธุ์ของแอททริบิวต์ นี่คือเมื่อฉันใช้เป็นการส่วนตัว@propertyและ@attribute.setter(ไม่เช่นนั้นจะไม่มี acessor / mutators ที่ชัดเจนสำหรับแอตทริบิวต์สาธารณะ)

สุดท้ายคำแนะนำ PEP8 และ Google Style Guide:

PEP8 การออกแบบเพื่อรับมรดกพูดว่า:

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

ในทางตรงกันข้ามตามที่ระบุไว้ในคู่มือสไตล์ของ Google Python ภาษากฎ / คุณสมบัติคำแนะนำคือ:

ใช้คุณสมบัติในรหัสใหม่เพื่อเข้าถึงหรือตั้งค่าข้อมูลที่ปกติแล้วคุณจะใช้วิธีการเข้าถึงที่เรียบง่ายน้ำหนักเบาหรือตัวตั้งค่า คุณสมบัติควรสร้างด้วย@propertyมัณฑนากร

ข้อดีของวิธีนี้:

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

และข้อเสีย:

ต้องสืบทอดมาจากobjectใน Python 2 สามารถซ่อนผลข้างเคียงได้เหมือนกับการใช้งานเกินพิกัด อาจสร้างความสับสนให้กับคลาสย่อย


1
ฉันไม่เห็นด้วยอย่างยิ่ง หากฉันมีคุณลักษณะ 15 อย่างบนวัตถุของฉันและฉันต้องการให้คำนวณด้วย@propertyการทำให้ส่วนที่เหลือใช้@propertyดูเหมือนการตัดสินใจที่ไม่ดี
Quelklef

เห็นด้วย แต่เฉพาะในกรณีที่มีบางอย่างเฉพาะเจาะจงเกี่ยวกับคุณลักษณะเฉพาะที่คุณต้องการ@property(เช่นดำเนินการตรรกะพิเศษบางอย่างก่อนส่งคืนแอตทริบิวต์) มิฉะนั้นแล้วทำไมคุณถึงต้องตกแต่งด้วยคุณลักษณะหนึ่งด้วย@properyและไม่ใช่อย่างอื่น?
Tomasz Bartkowiak

@Quelklef ดู sidenote ในโพสต์ (ทำเครื่องหมายด้วยดอกจัน)
Tomasz Bartkowiak

ถ้าคุณไม่ได้ทำสิ่งใดสิ่งหนึ่งที่ sidenote พูดถึงคุณก็ไม่ควร@propertyเริ่มด้วยใช่ไหม? หากทะเยอทะยานของคุณreturn this._xและตัวตั้งค่าของคุณthis._x = new_xแล้วการใช้@propertyเลยก็โง่นะ
Quelklef

1
อืมบางที โดยส่วนตัวฉันจะบอกว่ามันไม่เป็นไร --- มันฟุ่มเฟือยอย่างสิ้นเชิง แต่ฉันสามารถดูว่าคุณมาจากไหน ฉันเดาว่าฉันเพิ่งอ่านโพสต์ของคุณโดยบอกว่า "สิ่งที่สำคัญที่สุดเมื่อใช้@propertyคือความสอดคล้อง"
Quelklef

-1

คุณสามารถใช้วิธีมายากลและ__getattribute____setattr__

class MyClass:
    def __init__(self, attrvalue):
        self.myattr = attrvalue
    def __getattribute__(self, attr):
        if attr == "myattr":
            #Getter for myattr
    def __setattr__(self, attr):
        if attr == "myattr":
            #Setter for myattr

โปรดระวัง__getattr__และ__getattribute__ไม่เหมือนกัน __getattr__ถูกเรียกใช้เฉพาะเมื่อไม่พบคุณลักษณะ

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