Ачааллж байна...
Өмнөх хичээлд staticmethod функц class-ийн түвшинд, ямар нэг объектод хамааралгүйгээр байрладгийг харсан. Class-ийн түвшинд функц төдийгүй өгөгдөл ч байрлаж чадна. Class-ийн биед, ямар ч method-ын гадна оноосон хувьсагчийг class attribute гэж нэрлэдэг. Course 2-т та үүнийг тогтмол утга хадгалахад хэрэглэсэн байж магадгүй. Энэ хичээлд бид class attribute-ыг гүнзгий ойлгоно: тэр хаана амьдардаг, Python түүнийг хэрхэн олдог, хэзээ хэрэглэх нь зөв, хэзээ аюултай.
Ялгааг нэг өгүүлбэрээр хэлбэл: __init__ дотор self.name = name гэж оноосон instance attribute нь объект бүрт өөр өөр, харин class-ийн биед оноосон class attribute нь бүх объектод нэг, хуваалцагддаг. Энэ хуваалцах чанар нь class attribute-ын хүч ч, аюул ч. Тогтмол утга, тохиргоо, бүх объектын нийт тоо зэрэгт тэр яг тохирно. Гэвч хуваалцаж байгаагаа мэдэлгүй хэрэглэвэл нэг объектын өөрчлөлт бусад бүх объектод чимээгүй нөлөөлдөг.
Энэ хичээлийн төв хэсэг бол тэр аюул. Бид Python-ий хамгийн алдартай урхиудын нэгийг санаатайгаар үүсгэнэ: өөрчлөгдөж болох class attribute, жишээлбэл list, бүх объектын дунд хуваалцагдаж, нэг сурагчид нэмсэн шошго бүх сурагчид гарч ирэх. Python ямар ч алдаа өгөхгүй. Энэ нь тест, debug-д хамгийн хэцүү алдааны ангилалд багтдаг. Бид объект бүрийн дотоод төлөвийг алхам бүрт trace-ээр харж, асуудал хаана үүсч байгааг яг тодорхойлоод засна. Мөн Хичээл 4.2-ын "өөрчлөх ба солих"-ын ялгаа энд ч шийдвэрлэх үүрэгтэй болохыг харна.
Оффисын нэг хана руу харуулсан цагаан самбар төсөөлөөд үзээрэй. Самбар дээр бүх ажилтанд хамаатай мэдээлэл бичигдсэн: "Ажлын цаг: 9-18", “Wi-Fi нууц үг: ...”. Ажилтан бүр мөн өөрийн хувийн дэвтэртэй. Тэнд өөрийн нэр, даалгавар, тэмдэглэл бичдэг.
Ажилтан мэдээлэл хайхдаа эхлээд өөрийн дэвтрийг хардаг. Дэвтэрт байхгүй бол самбар руу хардаг. "Ажлын цаг хэд вэ?" гэж асуухад дэвтэрт бичээгүй тул самбараас уншина. Энэ бол Python-ий attribute хайлтын дүрэм: эхлээд объектын өөрийнх, дараа нь class-ийнх.
Одоо бичих үйлдэл. Хэрэв ажилтан "миний ажлын цаг 10-19" гэж бичвэл тэр өөрийн дэвтэрт бичнэ, самбарт биш. Дараа нь тэр ажлын цагийг хайхдаа дэвтрээсээ олох тул самбарын мэдээллийг дахин хэзээ ч харахгүй. Самбар өөрчлөгдөөгүй, бусад ажилтан хуучин цагийг харсаар. Самбар дээр бичихийн тулд самбар руу очиж, тэнд бичих хэрэгтэй.
Эцэст нь хамгийн нарийн хэсэг. Самбарын хажууд нийтийн хайрцаг байна: "Оффисын хэрэгсэл". Ажилтан хайрцгийг өөрийн ширээн дээр авчираад, дотор нь үзэг хийнэ. Тэр самбарт юу ч бичээгүй, гэвч нийтийн хайрцгийн дотрыг өөрчилсөн. Хайрцаг нэг тул бүх ажилтан дараа нь тэр үзгийг харна. Хичээл 4.2-ын "дэвтэрт бичих ба хайрцагт шинэ зүйл тавих"-ын ялгаа энд дахин гарч ирж байна.
Class attribute ба instance attribute хоёрыг зэрэгцүүлж, объект бүр юу хадгалдгийг харъя. Python-д объект бүр өөрийн attribute-уудыг __dict__ нэртэй dict-д хадгалдаг. Энэ бол зүйрлэлийн хувийн дэвтэр.
python
class Student:
school = "Улаанбаатар Апп"
def __init__(self, name):
self.name = name
bat = Student("Бат")
saraa = Student("Сараа")
print(bat.name, "|", bat.school)
print(saraa.name, "|", saraa.school)
print("bat-ийн дэвтэр:", bat.__dict__)
print("school class-д:", Student.__dict__["school"])Бат | Улаанбаатар Апп
Сараа | Улаанбаатар Апп
bat-ийн дэвтэр: {'name': 'Бат'}
school class-д: Улаанбаатар Аппschool нь class-ийн биед оноогдсон тул class attribute. name нь __init__ дотор self дээр оноогдсон тул instance attribute. Гурав дахь мөрийг анхааралтай хараарай: bat-ийн __dict__-д зөвхөн name байна, school байхгүй. Гэтэл bat.school ажилласан. Python bat-ийн дэвтэрт school-ийг хайгаад олоогүй, тэгээд Student class руу хараад олсон. Дөрөв дэх мөр school class-ийн __dict__-д, самбар дээр амьдардгийг баталж байна.
Хайлтын дүрэм: объект.нэр гэж уншихад Python эхлээд объектын __dict__-ийг, дараа нь түүний class-ийнхыг, дараа нь эцэг class-уудынхыг дарааллаар хайдаг. Эхний олдсон газраа зогсоно. Энэ нь Бүлэг 4-ийн LEGB-тэй төстэй боловч өөр, объектын attribute-д зориулсан шат.
Жишээ 1: объектын тоолуур — хууль ёсны хэрэглээ
Class attribute-ын хамгийн сонгодог хэрэглээ бол бүх объектод хуваалцагдах тоо: хичнээн сурагч бүртгэгдсэн бэ?
python
class Student:
count = 0
def __init__(self, name):
self.name = name
Student.count += 1
Student("Бат")
Student("Сараа")
Student("Ану")
print("Нийт:", Student.count)Нийт: 3Trace хийе. Энэ удаа объект бүрийн дэвтэр ба самбарыг алхам бүрт тэмдэглэнэ.
Алхам 1 (class тодорхойлогдох). Самбар: count = 0. Объект алга.
Алхам 2 (Student("Бат")). Шинэ объект, __init__ ажиллана. self.name = name нь Батын дэвтэрт name бичнэ. Student.count += 1 нь Student.count = Student.count + 1-ийн товчлол: самбараас 0-ийг уншаад, 1 болгоод, самбарт буцааж бичнэ, учир нь зүүн тал нь Student class. Самбар: count = 1. Батын дэвтэр: {name: "Бат"}.
Алхам 3-4 (Сараа, Ану). Ижил зам. Самбар: count = 3. Дэвтэр бүрт зөвхөн name.
Чухал зүйл: тоолуурыг шинэчлэхдээ class-ийн нэрээр, самбар руу шууд хандсан. Хэрэв self.count += 1 гэж бичвэл юу болох вэ? Энэ бол урхиудын эхнийх:
python
class Student:
count = 0
def __init__(self, name):
self.name = name
self.count += 1
bat = Student("Бат")
saraa = Student("Сараа")
print("Самбар:", Student.count)
print("bat.count:", bat.count, "| дэвтэр:", bat.__dict__)
print("saraa.count:", saraa.count, "| дэвтэр:", saraa.__dict__)Самбар: 0
bat.count: 1 | дэвтэр: {'name': 'Бат', 'count': 1}
saraa.count: 1 | дэвтэр: {'name': 'Сараа', 'count': 1}Самбар 0 хэвээр, сурагч бүр өөрийн дэвтэрт “1” гэж бичсэн. Trace: self.count += 1 нь self.count = self.count + 1. Баруун тал эхлээд бодогдоно: self.count-ийг уншина. Батын дэвтэрт count байхгүй тул самбараас 0 олдоно. 0 + 1 = 1. Дараа нь зүүн тал: self.count = 1 нь бичих үйлдэл, self-ээр, тэгэхээр Батын дэвтэрт. Зүйрлэлийн "өөрийн ажлын цагийг бичих" яг энэ: уншилт самбараас, бичилт дэвтэрт. Сараа ч мөн самбараас 0 уншиж, өөрийн дэвтэрт 1 бичнэ. Энэ үйлдлийг shadowing буюу "сүүдэрлэх" гэж нэрлэдэг: Батын дэвтэр дэх count самбарын count-ийг түүнээс нууж байна.
Хамгийн аюултай нь энэ алдаа ямар ч мессеж өгөхгүй, програм ажиллаж, зүгээр л буруу тоо гаргана. Дүрэм: class attribute-ыг шинэчлэхдээ үргэлж class-аар хандана. Энгийн method дотроос Student.count эсвэл type(self).count, classmethod дотроос cls.count.
Жишээ 2: тогтмол утга ба удамшилд дарж бичих
Class attribute-ын хоёр дахь хууль ёсны хэрэглээ бол бүх объектод нийтлэг тогтмол утга эсвэл тохиргоо. Дэвших оноог class attribute болгоё:
python
class Student:
PASSING_SCORE = 60
def __init__(self, name, score):
self.name = name
self.score = score
def passed(self):
return self.score >= self.PASSING_SCORE
class GraduateStudent(Student):
PASSING_SCORE = 75
bat = Student("Бат", 70)
anu = GraduateStudent("Ану", 70)
print(bat.name, bat.passed())
print(anu.name, anu.passed())Бат True
Ану FalseТом үсгээр бичсэн нэр бол Python-ий уламжлал: "энэ бол тогтмол, өөрчлөгдөх ёсгүй". Python үүнийг хүчээр шаарддаггүй, гэхдээ уншигчид чухал дохио өгнө. passed method self.PASSING_SCORE-ийг уншиж байна, Student.PASSING_SCORE биш. Хайлтын дүрэм: Батын дэвтэрт байхгүй, Student class-д 60. Аны дэвтэрт байхгүй, GraduateStudent class-д 75, хайлт тэнд зогсоно. Ингэснээр удамшсан class тогтмол утгыг дарж бичиж чадна. Энэ нь Хичээл 7.2-ын Жишээ 1-ийн staticmethod дарж бичилттэй яг ижил зарчим: self-ээр хандвал объектын жинхэнэ class-ийн хувилбар олдоно.
Мөн class attribute-ыг ажиллах үед өөрчилбэл тэр нь сүүдэрлээгүй бүх объектод нэг дор нөлөөлнө:
python
Student.PASSING_SCORE = 50
print(Student("Болд", 55).passed())TrueЭнэ нь тохиргоог бүх объектод нэг газраас өөрчлөх хүчтэй арга. Гэхдээ яг энэ хүч нь дараагийн жишээний аюулын эх үүсвэр.
Жишээ 3: урхи — өөрчлөгдөж болох class attribute
Одоо хичээлийн гол хэсэг. Сурагч бүр өөрийн шошгуудын жагсаалттай байх ёстой гэж бодъё: "идэвхтэй", "тэтгэлэгтэй" гэх мэт. Course 2-ын хэв маягаар хоосон жагсаалтыг class-ийн биед тодорхойлох уруу таталт гардаг:
python
class Student:
tags = []
def __init__(self, name):
self.name = name
def add_tag(self, tag):
self.tags.append(tag)
bat = Student("Бат")
saraa = Student("Сараа")
bat.add_tag("тэтгэлэгтэй")
print("Бат:", bat.tags)
print("Сараа:", saraa.tags)Бат: ['тэтгэлэгтэй']
Сараа: ['тэтгэлэгтэй']Бид зөвхөн Батад шошго нэмсэн. Гэвч Сараа ч "тэтгэлэгтэй" болжээ. Python ямар ч алдаа өгсөнгүй. Хэрэв энэ бол бодит програм байсан бол, тэтгэлэгийн мөнгө хуваарилдаг систем гэх мэт, үр дагавар нь ноцтой. Trace хийе.
Алхам 1 (class тодорхойлогдох). Python class-ийн биеийг нэг удаа ажиллуулж, хоосон list үүсгэнэ. Самбар: tags = [ ] (нэг list объект).
Алхам 2 (Student("Бат")). __init__ зөвхөн name бичнэ. Батын дэвтэр: {name: "Бат"}. tags дэвтэрт байхгүй.
Алхам 3 (Student("Сараа")). Сараагийн дэвтэр: {name: "Сараа"}. tags мөн байхгүй.
Алхам 4 (bat.add_tag("тэтгэлэгтэй")). self.tags.append(tag) мөр. Эхлээд self.tags-ийг уншина: Батын дэвтэрт байхгүй, самбараас олдоно, тэр нь нэг ижил list. Дараа нь тэр list дээр append дуудна. Энэ нь list-ийн дотрыг өөрчилдөг, self.tags нэрэнд шинэ утга оноодоггүй. Бичих үйлдэл байхгүй тул shadowing болохгүй, Жишээ 1-ээс ялгаатай. Самбар: tags = ["тэтгэлэгтэй"]. Батын дэвтэр: {name: "Бат"}, өөрчлөгдөөгүй.
Алхам 5 (saraa.tags уншигдах). Сараагийн дэвтэрт tags байхгүй, самбараас олдоно: ["тэтгэлэгтэй"].
Зүйрлэлийн "нийтийн хайрцагт үзэг хийх" яг энэ. Хоёр объект нэг list-ийг хуваалцаж байгааг шууд баталъя:
python
print(bat.tags is saraa.tags)
print(bat.tags is Student.tags)
print("Батын дэвтэр:", bat.__dict__)True
True
Батын дэвтэр: {'name': 'Бат'}Гурвуулаа нэг объект. Батын дэвтэрт tags огт байхгүй. Энэ хоёр жишээг харьцуулбал Хичээл 4.2-ын сургамж бүрэн давтагдаж байна: Жишээ 1-ийн += нь тоон дээр солих үйлдэл, тиймээс объектын дэвтэрт шинэ бичлэг үүсч, самбар хамгаалагдсан. Жишээ 3-ын append нь list дээр өөрчлөх үйлдэл, тиймээс самбар дээрх нийтийн объект өөрчлөгдсөн. Ижил хэлбэртэй код, огт өөр үр дагавар.
Засвар энгийн: объект бүрт өөрийн жагсаалтыг __init__ дотор үүсгэх.
python
class Student:
def __init__(self, name):
self.name = name
self.tags = []
def add_tag(self, tag):
self.tags.append(tag)
bat = Student("Бат")
saraa = Student("Сараа")
bat.add_tag("тэтгэлэгтэй")
print("Бат:", bat.tags)
print("Сараа:", saraa.tags)
print(bat.tags is saraa.tags)
print("Батын дэвтэр:", bat.__dict__)Бат: ['тэтгэлэгтэй']
Сараа: []
False
Батын дэвтэр: {'name': 'Бат', 'tags': ['тэтгэлэгтэй']}__init__ объект бүр үүсэх үед ажилладаг тул self.tags = [] бүрт шинэ хоосон list үүсгэж, объектын дэвтэрт бичнэ. Хоёр list өөр объект, is шалгалт False. Батын дэвтэрт tags харагдаж байна. Дүрэм: list, dict, set зэрэг өөрчлөгдөж болох өгөгдөл, объект бүрт өөр байх ёстой бол заавал __init__ дотор үүсгэнэ. Class-ийн биед зөвхөн бүх объектод үнэхээр нэг байх ёстой зүйлс: тогтмол утга, тохиргоо, тоолуур.
Жишээ 4: ах дүү урхи — өөрчлөгдөж болох анхдагч argument
Ижил асуудал өөр хэлбэрээр гарч ирдэг бөгөөд үүнийг энд танилцуулах нь зүйтэй. Сурагчийг анхны шошготой үүсгэх боломж өгөх гэж __init__-д анхдагч утга өгье:
python
class Student:
def __init__(self, name, tags=[]):
self.name = name
self.tags = tags
bat = Student("Бат")
saraa = Student("Сараа")
bat.tags.append("тэтгэлэгтэй")
print("Сараа:", saraa.tags)
print(bat.tags is saraa.tags)Сараа: ['тэтгэлэгтэй']
TrueОдоо tags __init__ дотор self дээр оноогдож байгаа ч асуудал буцаж ирлээ. Шалтгаан нь Хичээл 6.4-т тэмдэглэсэн дүрэм: анхдагч утга def мөр ажиллах үед нэг удаа бодогддог. tags=[] нь функц тодорхойлогдох үед нэг list үүсгэж, түүнийг хадгалдаг. Tags өгөгдөөгүй дуудлага бүр тэр нэг list-ийг авна. Хичээл 6.4-т retry-ийн sleep=time.sleep анхдагч утга аюулгүй байсан, учир нь функц өөрчлөгддөггүй. List өөрчлөгддөг.
Python-ий уламжлалт засвар нь None-ыг "өгөгдөөгүй" гэсэн дохио болгон ашиглах:
python
class Student:
def __init__(self, name, tags=None):
self.name = name
self.tags = [] if tags is None else list(tags)tags өгөгдөөгүй бол None ирж, __init__ шинэ хоосон list үүсгэнэ. Өгөгдсөн бол list(tags) нь түүний хуулбар-ыг үүсгэнэ, ингэснээр гаднаас өгсөн list-ийг дараа нь өөрчилсөн ч объектод нөлөөлөхгүй. Энэ None загвар Python кодонд маш түгээмэл тул та хаа сайгүй харна. Хичээл 7.4-т dataclass энэ асуудлыг илүү цэвэрхэн, field(default_factory=list)-ээр шийддэгийг харна. Тэр хэрэгслийн яагаад оршин байгааг та одоо бүрэн ойлгож байна.
Хүсвэл Жишээ 3-ын урхитай хувилбарт гурав дахь сурагч нэмж, Student.tags-ийг шууд хэвлэж үзээрэй: самбар дээр юу байх вэ? Сонирхвол bat.tags = ["шинэ"] гэж append биш, шууд оноож үзээд, дараа нь bat.__dict__, saraa.tags-ийг харах нь Жишээ 1-ийн shadowing ба Жишээ 3-ын хуваалцлагыг нэг дор харьцуулах сайн туршилт. Оноолт ба append хоёр юугаар ялгагдах вэ? Мөн Жишээ 1-ийн тоолуурт GraduateStudent нэмж, Student.count ба type(self).count-ийн аль нь удамшсан объектуудыг хэрхэн тоолохыг харьцуулж болно. Эдгээр бүгд санал бөгөөд дараагийн хичээлүүд үүнийг хийсэн гэж үзэхгүй.
Объект бүр өөрчлөлтийг хуваалцах. Мессежгүй, энэ хичээлийн гол урхи. Class-ийн биед list, dict, set тодорхойлж, дараа нь объектоор append, update, add хийхэд бүх объект нэг өгөгдлийг хуваалцана. Шинж тэмдэг: нэг объектод нэмсэн зүйл бусад объектод гэнэт харагдах. Шалгалт: a.attr is b.attr True гарвал хуваалцаж байна. Шийдэл: өөрчлөгдөж болох өгөгдлийг __init__ дотор self.attr = [] гэж үүсгэх.
Тоолуур өсөхгүй байх. Мессежгүй, Жишээ 1-ийн shadowing урхи. Class attribute-ыг self.count += 1 гэж шинэчлэхэд объект бүр өөрийн хуулбарт бичиж, class-ийн утга өөрчлөгдөхгүй. Шинж тэмдэг: Student.count 0 хэвээр, объект бүрийн count 1. Шалгалт: obj.__dict__-д тэр нэр харагдвал объект сүүдэрлэсэн. Шийдэл: class-аар бичих: Student.count += 1 эсвэл type(self).count += 1.
UnboundLocalError: cannot access local variable ‘count’ where it is not associated with a value. "Утга холбогдоогүй локал хувьсагч ‘count’-д хандах боломжгүй" гэсэн утгатай. Method дотор угтваргүйгээр count += 1 гэж бичихэд гарна. Хичээл 7.2-ын Жишээ 4-ийг санаарай: class-ийн бие Enclosing түвшин биш тул method count-ийг class-аас харахгүй. += оноолт тул Хичээл 4.2-ын дүрмээр count локал хувьсагч болж, утга оноогдохоос өмнө уншигдана. Хоёр бүлгийн мэдлэг нэг алдаанд нийлж байна. Шийдэл: Student.count += 1.
AttributeError: type object ‘Student’ has no attribute ‘name’. “Student гэдэг type объектод ‘name’ attribute байхгүй” гэсэн утгатай. Instance attribute-д class-аар хандахад гарна: Student.name. name нь объект бүрийн дэвтэрт, __init__-ээр бичигддэг, самбар дээр биш. Хайлтын дүрэм объектоос class руу явдаг, эсрэгээр биш: class объектуудын дэвтрийг хардаггүй. Мессеж дэх “type object” нь Python class-уудыг “type” төрлийн объект гэж нэрлэдгээс ирсэн. Шийдэл: объект үүсгэж, түүгээр хандах.
Анхдагч list бүх дуудлагад хуваалцагдах. Мессежгүй, Жишээ 4-ийн урхи. def __init__(self, tags=[]) эсвэл ямар ч функцэд items=[], options={} анхдагч утга. Шинж тэмдэг нь Жишээ 3-тай ижил боловч class attribute байхгүй тул илүү будлиантай. Шийдэл: tags=None ба [] if tags is None else list(tags). ruff зэрэг linter-ууд өөрчлөгдөж болох анхдагч утгыг илрүүлэх дүрэмтэй байдаг.
Class-ийн биед оноосон class attribute бүх объектод нэг бөгөөд class-ийн __dict__-д амьдардаг. __init__ дотор self дээр оноосон instance attribute объект бүрийн өөрийн __dict__-д амьдардаг. объект.нэр уншихад Python эхлээд объектын dict-ийг, дараа нь class-ийнхыг, дараа нь эцэг class-уудынхыг хайна. Харин объект.нэр = утга бичих нь үргэлж объектын dict-д бичигдэж, class attribute-ыг сүүдэрлэнэ. Тиймээс тоолуур зэрэг class attribute-ыг шинэчлэхдээ class-аар хандана. Тогтмол утга, тохиргоо class attribute-д тохирно бөгөөд тэдгээрийг self-ээр уншвал удамшсан class дарж бичиж чадна. Өөрчлөгдөж болох class attribute-ын дотрыг объектоор өөрчлөх нь shadowing үүсгэдэггүй тул бүх объект тэр өөрчлөлтийг чимээгүй хуваалцана. Ийм өгөгдлийг __init__ дотор үүсгэнэ. Ижил урхи өөрчлөгдөж болох анхдагч argument-д ч гарна, учир нь анхдагч утга def мөрөнд нэг удаа бодогддог. None загвараар шийддэг.
Хичээл 4.2-оос эхэлсэн "өөрчлөх ба солих"-ын ялгаа одоо гурван өөр нөхцөлд гарч ирлээ: closure-ийн хайрцаг, class-ийн самбар, анхдагч argument. Энэ бол Python-ий нэр ба объектын харилцааны нэг гол санаа. Дараагийн хичээлд бид dataclass-тай дахин уулзаж, түүний гурван гүн боломжийг судална: объектыг өөрчлөгдөхгүй болгодог frozen, энэ хичээлийн анхдагч утгын урхийг зөв шийддэг field(default_factory=...), мөн __init__-ийн дараа шалгалт ба тооцоолол хийх боломж олгодог __post_init__.
Бүртгэлтэй болсноор энэ сургалтын бүх хичээлд хандах эрх авна.