۰
عکس از Dan Meyers در Unsplash
جنگو امکانات کلان زیادی دارد که زندگی ما را به عنوان توسعهدهندهها آسانتر میکند، و من پیشتر مقالات زیادی در این زمینه نوشتهام. اما در این مقاله میخواهم روی آن نکات کوچک و مفید در کدنویسی تمرکز کنم که معمولاً کمتر مورد توجه قرار میگیرند ولی میتوانند در برنامهنویسی روزمره به شدت کارآمد باشند.
در ادامه، فهرست ۱۰ نکته شخصی من که باید در جنگو بدانید را آوردهام — نکاتی که باعث میشوند کد شما سادهتر و زیباتر شود.
علاوه بر متدهای بسیار رایج filter()
, exclude()
و annotate()
، جنگو متدهای کمتر شناخته شدهای مثل first()
و last()
را هم ارائه میدهد که بهعنوان syntactic sugar (شکرگزاری نحوی برای خوانایی بهتر) کمک میکنند.
همانطور که از نامشان پیداست، first()
اولین رکورد در QuerySet را برمیگرداند و last()
آخرین رکورد را. شخصاً معمولاً اینها را در تستهای واحد استفاده میکنم.
مثلاً کد زیر را در نظر بگیرید که اولین رکورد Fruit
را میگیرد:
1>>> Fruit.objects.all()[:1][0] 2<Fruit: Banana ($0.99)>
میتوان همان را اینگونه با first()
نوشت:
1>>> Fruit.objects.first() 2<Fruit: Banana ($0.99)>
میبینید که کد بسیار خواناتر شده است. مزیت دیگر این متدها این است که اگر QuerySet خالی باشد، به جای اندیسدهی که خطا ایجاد میکند، مقدار None
برمیگردانند:
1# وقتی از برش استفاده میکنیم، باید خطاگیری کنیم چون ممکن است QuerySet خالی باشد: 2>>> Fruit.objects.filter(price__gt=5.00)[:1][0] 3Traceback (most recent call last): 4 File "<console>", line 1, in <module> 5 File ".../site-packages/django/db/models/query.py", line 318, in __getitem__ 6 return qs._result_cache[0] 7IndexError: list index out of range 8 9# اما با first() یا last() مقدار None برگردانده میشود: 10>>> Fruit.objects.filter(price__gt=5.00).first() 11None
برای اطلاعات بیشتر و متدهای دیگر میتوانید به مستندات QuerySets جنگو مراجعه کنید.
دو متد بسیار مفید دیگر که مرتب استفاده میکنم، get_or_create()
و update_or_create()
هستند.
این متدها حجم زیادی از کدهای تکراری را پاک میکنند و همزمان خوانایی کد را بهتر میکنند:
1# بدون استفاده از get_or_create: 2try: 3 fantasy = Category.objects.get(name='Fantasy') 4except Category.DoesNotExist: 5 fantasy = Category.objects.create(name='Fantasy') 6 created = True 7 8# با get_or_create: 9fantasy, created = Category.objects.get_or_create(name='Fantasy')
هر دو متد یک Tuple به شکل (رکورد، بولین)
برمیگردانند که در آن عنصر اول رکورد مربوطه است و عنصر دوم True
میشود اگر رکورد جدید ایجاد شده باشد.
مثال مشابه با update_or_create()
:
1# بدون استفاده از update_or_create: 2try: 3 horror = Category.objects.get(name='Horror') 4 horror.name = 'Scary' 5 horror.save() 6except Category.DoesNotExist: 7 horror = Category.objects.create(name='Scary') 8 created = True 9 10# با update_or_create: 11horror, created = Category.objects.update_or_create(name='Horror', defaults={'name': 'Scary'})
در اینجا هم کل try-except به یک فراخوانی متد تبدیل شده است.
وقتی در مدل جنگو فیلدی با گزینه choices
تعریف میکنید، معمولاً میخواهیم مقدار قابل خواندن توسط کاربر را در قالب (template) چاپ کنیم.
مثلاً مدل زیر را در نظر بگیرید:
1class Book(models.Model): 2 STATUSES = [ 3 (1, "Available"), 4 (2, "Signed out"), 5 (3, "On Backorder"), 6 (4, "To Be Shelved"), 7 (5, "Out of Circulation"), 8 ] 9 title = models.CharField(max_length=128) 10 author = models.ForeignKey(Author, on_delete=models.CASCADE) 11 category = models.ForeignKey(Category, on_delete=models.CASCADE) 12 status = models.IntegerField(choices=STATUSES)
اگر بخواهیم در قالب مقدار status
را چاپ کنیم با {{ book.status }}
فقط عدد نمایش داده میشود. برای گرفتن مقدار قابل فهم کاربری میتوانیم از متد get_status_display
استفاده کنیم:
1<h1>Book Entry: {{ book.title }}</h1> 2<b>Author:</b> {{ book.author }}<br /> 3<b>Status:</b> {{ book.get_status_display }}<br />
نتیجه HTML به شکل زیر خواهد بود:
1<h1>Book Entry: Moby Dick</h1> 2<b>Author:</b> Herman Melville<br /> 3<b>Status:</b> Signed Out<br />
عملیات ایجاد و بهروزرسانی دستهای (bulk) در جنگو فقط به خاطر آسان بودن و خوانایی کد قدرتمند نیست، بلکه کل فرآیند به صورت اتمی انجام میشود.
برای ایجاد دستهای، کافی است لیستی از اشیاء مدل را بسازید و آن را به bulk_create()
بدهید:
1new_fruits = [ 2 Fruit(name='Banana', price=0.80, description='Bunch of 4'), 3 Fruit(name='Banana (organic)', price=0.99, description='Bunch of 4'), 4 Fruit(name='Apple', price=0.75, description='Honeycrisp'), 5 Fruit(name='Strawberry', price=0.10), 6] 7fruits = Fruit.objects.bulk_create(new_fruits)
برای بهروزرسانی دستهای نیز میتوانید از متد update()
روی QuerySet استفاده کنید:
1# بهروزرسانی description برای تمام میوههایی که قیمتشان بالای ۱ دلار است: 2>>> Fruit.objects.filter(price__gt=1.00).update(description='Too expensive!') 35
عدد بازگشتی تعداد رکوردهایی است که آپدیت شدند.
جنگو دو قابلیت بسیار کاربردی برای مدیریت تاریخها ارائه میدهد: auto_now
و auto_now_add
.
auto_now
مقدار فیلد را در هر بار ذخیره شدن رکورد به زمان جاری بهروزرسانی میکند.auto_now_add
فقط در زمان ایجاد اولیه رکورد مقداردهی میشود.مثلاً مدل زیر:
1class Article(models.Model): 2 title = models.CharField(max_length=128) 3 created_on = models.DateTimeField(auto_now_add=True) 4 last_modified = models.DateTimeField(auto_now=True)
مشاهده میکنیم که فیلد last_modified
هر بار که مدل ذخیره میشود به روز میشود ولی created_on
ثابت میماند:
1>>> a = Article.objects.create(title='This is an example') 2>>> a.created_on 3datetime.datetime(2024, 7, 15, 21, 12, 44, 580527, tzinfo=<UTC>) 4>>> a.last_modified 5datetime.datetime(2024, 7, 15, 21, 12, 44, 580628, tzinfo=<UTC>) 6 7# اکنون شی را بهروز میکنیم: 8>>> a.title = 'I have been updated' 9>>> a.save() 10 11# توجه کنید last_modified بهروزرسانی شده ولی created_on ثابت مانده: 12>>> a.created_on 13datetime.datetime(2024, 7, 15, 21, 12, 44, 580527, tzinfo=<UTC>) 14>>> a.last_modified 15datetime.datetime(2024, 7, 15, 21, 13, 01, 538356, tzinfo=<UTC>)
علاوه بر متدهای assert عمومی Python مثل assertEqual
, assertTrue
, assertIsNone
, assertIn
و … جنگو مجموعهای از متدهای assert مخصوص و بسیار کاربردی دارد که در تستهایتان خیلی به درد میخورند. محبوبترینها:
assertContains
— بررسی میکند رکورد مشخصی در یک QuerySet یا خروجی وجود دارد. عکس آن، assertNotContains
.assertURLEqual
— برای مقایسه URLها که گاهی ترتیب پارامتر GET فرق دارد ولی URL معادل است.assertFormError
— بررسی خطاهای فرم در ارسالها، برای تست اعتبارسنجیها خیلی مفید است.assertTemplateUsed
— بررسی میکند یک ویو هنگام رندر کردن از قالب خاصی استفاده کرده باشد.ادامه موضوع تست به یک قابلیت جالب میرسیم: دکوراتور @tag
که به توسعهدهندهها اجازه میدهد تستها را دستهبندی کنند.
هدف این است که هنگام اجرای تستها بتوانیم فقط زیرمجموعهای از آنها را با توجه به دستهبندیشان اجرا کنیم.
استفاده آسان است، فقط دکوراتور @tag
را روی کلاس یا تابع تست قرار میدهیم:
1from django.test import tag 2 3@tag("api", "core") 4class ExampleAPITest(TestCase): 5 def setUp(self): 6 pass 7 8 def test_example(self): 9 # منطق تست اینجا
و هنگام اجرای تستها پارامتر --tag
را میدهیم تا فقط تستهای آن تگ اجرا شوند:
1python manage.py test --tag=api
جنگو تگهای داخلی بسیار مفیدی دارد که برخی کمتر استفاده میشوند ولی بهتر است به یاد داشته باشیم:
join
مانند متد join
پایتون برای رشتهها عمل میکند.first
و last
اولین و آخرین مقدار موجود در لیست را میدهد.floatformat
تعداد ارقام بعد از اعشار را کنترل میکند، مثلاً برای قیمتها مفید است:
1{{ price|floatformat:2 }}
intcomma
برای خوانایی اعداد بزرگ، ویرگولها را اضافه میکند: 1000000
تبدیل میشود به 1,000,000
.linebreaksbr
خطهای جدید را به <br />
در HTML تبدیل میکند.urlize
خودکار متن URL را به تگ <a>
لینک تبدیل میکند.yesno
تبدیل مقدار بولی به رشته کاربرپسند است، مثلاً:
مقدار1{{ record.is_active|yesno:"Active,Disabled" }}
True
به Active
و False
به Disabled
تبدیل میشود.فهرست کامل template tags و filters
این نکته اختصاصی به این دلیل است که من دیرتر یاد گرفتمش و کلی زمان را ذخیره کرده است.
اگر یک نمونه مدل (instance) دارید و فکر میکنید دادهاش کَهنه شده و میخواهید از دیتابیس تازهاش را بگیرید، کافی است این متد را صدا بزنید:
1fruit = Fruits.objects.last() 2 3# بدون refresh_from_db: 4fruit = Fruits.objects.get(pk=fruit.pk) 5 6# با refresh_from_db: 7fruit.refresh_from_db()
من در تستهای REST API خیلی از این متد استفاده میکنم چون میخواهم از آخرین وضعیت رکورد اطمینان حاصل کنم.
آخرین نکته آسان است — جنگو یک دکوراتور @admin.register
در اختیار گذاشته که باعث میشود کدهای مربوط به پنل مدیریت نزدیکتر به هم و خواناتر شوند:
1from django.contrib import admin 2 3# بدون دکوراتور: 4class FruitAdmin(admin.ModelAdmin): 5 search_fields = ['name', 'description'] 6 list_filter = ['name'] 7 list_display = ('name', 'price', 'description') 8 9admin.site.register(Fruit, FruitAdmin) 10 11# با دکوراتور: 12@admin.register(Fruit) 13class FruitAdmin(admin.ModelAdmin): 14 search_fields = ['name', 'description'] 15 list_filter = ['name'] 16 list_display = ('name', 'price', 'description')
اگرچه فقط یک میانبر است، ولی به شدت خوانایی را بهتر میکند و ثبت کلاس مدیریت را در کنار تعریف کلاس نگه میدارد.
انتخاب فقط ۱۰ نکته از بین انبوه قابلیتهای جنگو سخت بود، ولی تمرکز کردم روی مواردی که خودم زیاد استفاده میکنم و بیشترین صرفهجویی را در کد دارند. اگر نکته یا ترفند خاصی دارید که اشاره نشده، حتماً در بخش نظرات به اشتراک بگذارید!
۰
کد با می متعهد است که بالاترین سطح کیفی آموزش را در اختیار شما بگذارد. هدف به اشتراک گذاشتن دانش فناوری اطلاعات و توسعه نرم افزار در بالاترین سطح ممکن برای درستیابی به جامعه ای توانمند و قدرتمند است. ما باور داریم هر کسی میتواند با استمرار در یادگیری برنامه نویسی چالش های خود و جهان پیرامون خود را بر طرف کند و به موفقیت های چشم گیر برسد. با ما در این مسیر همراه باشید. کد با می اجتماع حرفه ای برنامه نویسان ایرانی.