В какой-то момент времени я превратился в педанта брюзгу. В фильмах малейшие нестыковки и провалы в логике портят мне весь просмотр. В чатах меня бесит it's вместо its. А в статьях про программирование... Всё плохо. За меня всё уже сказал @AlexanderAstafiev, я лишь процитирую:
Простите, я не могу так больше. Я слишком хорошо знаю Python, чтобы молчать при виде такого кода. Я устал. Я не могу это читать. Простите за токсичную критику, накипело.
Самое забавное, что, по моим ощущениям, везде я вижу одни и те же классы проблем. Я даже запилил сервис, где можно закинуть код и получить код ревью, и, собрав немного статистики, понял, что 50 типов ошибок достаточно, чтобы покрыть большую часть проблем в чужом коде. Но выборка у меня была небольшая, и я подумал: а что, если проверить много кода? И всё заверте...
разработчик с мозгом груга не очень умный, но разработчик с мозгом груга программирует много лет и научился кое чему, хоть всё равно часто запутывается
Я как прочитал её, так сразу и понял: это я. Не очень умный программист, но за плечами горы опыта, и это формирует мой подход к написанию кода: не усложняй. На работе у нас есть чел - большая голова, он держит весь проект в своей "оперативной памяти" и пишет просто полотна кода с кучей ветвлений, и где-то в конце он ещё помнит, что в начале был if и нужно рассмотреть else. Я так не могу, я когда прыгаю jump-to-definition, на четвёртом уровне вложенности уже забываю, откуда я пришёл и что я делал. Я как бельчонок с хорошеньким хвостиком, если вы понимаете, о чём я.
Что же касается бельчонка, то он сидел в лесной чаще и рассеянно поглядывал то на одно дерево, то на другое. Он не мог, даже если бы пришлось пожертвовать своим хвостиком, вспомнить, в дупле какого дерева он жил и вообще ради чего он прискакал в лес и что там искал.
Я не могу писать большие и сложные штуки, а если и напишу, то там же и умру. Поэтому философия гругов мне очень подходит:
главный охотник на груга это сложность сложность плохо скажу снова: сложность очень плохо теперь ты скажи сам: сложность очень, очень плохо если выбрать между сложность и биться с динозавром, груг выбери динозавр: груг хотя бы видеть динозавра
И в этой статье я хотел немного поделиться тем, как можно бороться со сложностью в питоне.
Разработчик с мозгом груга хочет собрать знания в маленькую, простую и интересную страницу
Если вы умный
... то всё равно нужно стремиться упрощать. Потому что если вы пишете статью или вкладываете что-то в опенсорс, ваш код (внезапно) читают и/или запускают. Давайте заботиться о пользователях вашего кода? Кстати, пользователем вашего когда можете стать вы сами через несколько лет.
Что значит "упрощать" и "заботиться о пользователях"?
Я понимаю это как написание кода, который
легко читать
легко изменять
легко отлаживать
легко тестировать
Как писать такой код? Использовать линтеры и мозг, чтобы уменьшать сложность. Давайте обсудим типичные ошибки и антипаттерны.
Ты не сможешь рассмотреть все возможные ошибки
Действительно, выстрелить себе в ногу можно миллионом способов, особенно в динамически типизированном языке. Но мне поможет мой хоуми aka Николай Гумилёв Вильфредо Парето.
Закон Парето
В большинстве случаев около 80% результата происходят из 20% причин.
А вдруг мы можем рассмотреть топ 20% ошибок и надеяться, что покроем 80% случаев?
А как ты узнаешь самые распространённые ошибки?
Автоматически.
Вообще не всё можно отловить программно. Например, плохой дизайн классов и модулей. Как-то я видел что-то вроде:
Не знаю, какой там был контекст, но если в зависимости от какого-то параметра вызывается либо родительский метод, либо метод наследника, то, возможно, где-то на уровне выше нужно было в зависимости от параметра передать либо объект родителя, либо объект наследника - наследование так и работает, оно переопределяет метод, вместо того чтобы заставлять вас писать if. Как подобное отлавливать - я не знаю. Это самый интересный класс ошибок, но в этой статье их не будет, сорян.
Итак, как автоматически искать ошибки? Для этого я построю вундервафлю, прям как аннигилятор в "Полицейский из Беверли-Хиллз 3".
Если эта штука напоминает вам flake8 с плагинами, то вы угадали - это он и есть.
Почему не pylint | mypy | dlint | black | ...? Мне нужен был линтер (не форматтер), в котором есть максимальное число подходящих мне плагинов, и для которого я мог бы легко написать свои. Flake8 мне показался наиболее подходящим.
Итак, я прошёлся по awesome flake8, поскрёб по закоулкам гитхаба, написал свой мини-плагин с тем, чего не хватало, и получилось вот это:
Я постарался отобрать те плагины, которые сделают код чище, но не стал включать спорные штуки (например, всякие следилки за докстрингами) и базовые и очевидные вещи (форматирование, всякие там отступы и пробелы). Даже среди того, что я выбрал, было много мусора, поэтому я отбирал ошибки поштучно, если интересно - вот мой .flake8, он человекочитаем:
.flake8
[flake8]
# statistics = True
select =
# -------- https://github.com/best-doctor/flake8-adjustable-complexity --------
CAC001,
# func is too complex (complexity > max allowed complexity)
CAC002,
# func is too complex (complexity). Bad variable names penalty is too high (penalty)
# -------- https://github.com/best-doctor/flake8-annotations-coverage --------
TAE001,
# Too few type annotations in file
# -------- https://github.com/sco1/flake8-annotations --------
ANN001,
# Missing type annotation for function argument
ANN002,
# Missing type annotation for *args
ANN003,
# Missing type annotation for **kwargs
; ANN101,
# Missing type annotation for self in method
; ANN102,
# Missing type annotation for cls in classmethod
ANN201,
# Missing return type annotation for public function
ANN202,
# Missing return type annotation for protected function
ANN203,
# Missing return type annotation for secret function
ANN204,
# Missing return type annotation for special method
ANN205,
# Missing return type annotation for staticmethod
ANN206,
# Missing return type annotation for classmethod
ANN301,
# PEP 484 disallows both type annotations and type comments
ANN401,
# Dynamically typed expressions (typing.Any) are disallowed.2
# -------- https://github.com/gforcada/flake8-builtins --------
A001,
# variable "max" is shadowing a python builtin
A002,
# argument "dict" is shadowing a python builtin
# -------- https://github.com/Melevir/flake8-cognitive-complexity --------
CCR001,
# Cognitive complexity is too high (X > Y)
# -------- https://github.com/PyCQA/flake8-commas --------
# C812, # too many FPs
# missing trailing comma
C813,
# missing trailing comma in Python 3
# C814,
# missing trailing comma in Python 2
C815,
# missing trailing comma in Python 3.5+
C816,
# missing trailing comma in Python 3.6+
C818,
# trailing comma on bare tuple prohibited
C819,
# trailing comma prohibited
# -------- https://github.com/adamchainz/flake8-comprehensions --------
C400,
C401,
C402,
# Unnecessary generator - rewrite as a <list/set/dict> comprehension.
C403,
C404,
# Unnecessary list comprehension - rewrite as a <set/dict> comprehension.
C405,
C406,
# Unnecessary <list/tuple> literal - rewrite as a <set/dict> literal.
# C408,
# Unnecessary <dict/list/tuple> call - rewrite as a literal.
C409,
C410,
# Unnecessary <list/tuple> passed to <list/tuple>() - (remove the outer call to <list/tuple>``()/rewrite as a ``<list/tuple> literal).
C411,
# Unnecessary list call - remove the outer call to list().
C413,
# Unnecessary <list/reversed> call around sorted().
C414,
# Unnecessary <list/reversed/set/sorted/tuple> call within <list/set/sorted/tuple>().
C415,
# Unnecessary subscript reversal of iterable within <reversed/set/sorted>().
C416,
# Unnecessary <list/set> comprehension - rewrite using <list/set>().
C417,
# Unnecessary map usage - rewrite using a generator expression/<list/set/dict> comprehension.
# -------- https://github.com/wemake-services/flake8-eradicate --------
# E800,
# Found commented out code
# -------- https://github.com/best-doctor/flake8-expression-complexity --------
ECE001,
# Expression is too complex (X > Y)
# -------- https://github.com/best-doctor/flake8-functions --------
CFQ001,
# function length (default max length is 100)
# CFQ002,
# function arguments number (default max arguments amount is 6)
CFQ003,
# function is not pure.
; CFQ004,
# function returns number (default max returns amount is 3)
# -------- https://github.com/MartinThoma/flake8-simplify --------
# Python-specific rules:
SIM101,
# Multiple isinstance-calls which can be merged into a single call by using a tuple as a second argument (example)
SIM104,
#: Use 'yield from iterable' (introduced in Python 3.3, see PEP 380) (example)
SIM105,
# Use 'contextlib.suppress(...)' instead of try-except-pass (example)
SIM107,
# Don't use return in try/except and finally (example)
SIM108,
# Use the ternary operator if it's reasonable (example)
SIM109,
# Use a tuple to compare a value against multiple values (example)
SIM110,
# Use any(...) (example)
SIM111,
# Use all(...) (example)
SIM113,
# Use enumerate instead of manually incrementing a counter (example)
SIM114,
# Combine conditions via a logical or to prevent duplicating code (example)
SIM115,
# Use context handler for opening files (example)
SIM116,
# Use a dictionary instead of many if/else equality checks (example)
SIM117,
# Merge with-statements that use the same scope (example)
SIM119,
# Moved to flake8-scream due to issue 63
SIM120,
#: Use 'class FooBar:' instead of 'class FooBar(object):' (example)
SIM121,
# Reserved for SIM908 once it's stable
SIM125,
# Reserved for SIM905 once it's stable
SIM126,
# Reserved for SIM906 once it's stable
SIM127,
# Reserved for SIM907 once it's stable
# Simplifying Comparisons:
SIM201,
# Use 'a != b' instead of 'not a == b' (example)
SIM202,
# Use 'a == b' instead of 'not a != b' (example)
SIM203,
# Use 'a not in b' instead of 'not (a in b)' (example)
SIM204,
# Moved to flake8-scream due to issue 116
SIM205,
# Moved to flake8-scream due to issue 116
SIM206,
# Moved to flake8-scream due to issue 116
SIM207,
# Moved to flake8-scream due to issue 116
SIM208,
# Use 'a' instead of 'not (not a)' (example)
SIM210,
# Use 'bool(a)' instead of 'True if a else False' (example)
SIM211,
# Use 'not a' instead of 'False if a else True' (example)
SIM212,
# Use 'a if a else b' instead of 'b if not a else a' (example)
SIM220,
# Use 'False' instead of 'a and not a' (example)
SIM221,
# Use 'True' instead of 'a or not a' (example)
SIM222,
# Use 'True' instead of '... or True' (example)
SIM223,
# Use 'False' instead of '... and False' (example)
SIM224,
# Reserved for SIM901 once it's stable
SIM300,
# Use 'age == 42' instead of '42 == age' (example)
# Simplifying usage of dictionaries:
SIM401,
# Use 'a_dict.get(key, "default_value")' instead of an if-block (example)
SIM118,
# Use 'key in dict' instead of 'key in dict.keys()' (example)
# General Code Style:
SIM102,
# Use a single if-statement instead of nested if-statements (example)
SIM103,
# Return the boolean condition directly (example)
SIM106,
# Handle error-cases first (example). This rule was removed due to too many false-positives.
SIM112,
# Use CAPITAL environment variables (example)
SIM122,
#/ SIM902: Moved to flake8-scream due to issue 125
SIM123,
#/ SIM902: Moved to flake8-scream due to issue 130
SIM124,
# Reserved for SIM909 once it's stable
# Current experimental rules:
SIM901,
# Use comparisons directly instead of wrapping them in a bool(...) call (example)
SIM904,
# Assign values to dictionary directly at initialization (example)
SIM905,
# Split string directly if only constants are used (example)
SIM906,
# Merge nested os.path.join calls (example)
SIM907,
# Use Optional[Type] instead of Union[Type, None] (example)
SIM908,
# Use dict.get(key) (example)
SIM909,
# Avoid reflexive assignments (example)
# -------- https://github.com/MartinThoma/flake8-scream --------
SCR119,
#: Use dataclasses for data containers (example)
SCR902,
# Use keyword-argument instead of magic boolean (example)
SCR903,
# Use keyword-argument instead of magic number (example)
# -------- https://github.com/i02sopop/flake8-global-variables --------
# W001,
# Global variable {0} defined
W002,
# Global variable {0} used
# ---- https://github.com/PyCQA/pep8-naming ----
# N801,
# class names should use CapWords convention (class names)
# N802,
# function name should be lowercase (function names)
# N803,
# argument name should be lowercase (function arguments)
N804,
# first argument of a classmethod should be named 'cls' (function arguments)
N805,
# first argument of a method should be named 'self' (function arguments)
N806,
# variable in function should be lowercase
N807,
# function name should not start and end with '__'
N811,
# constant imported as non constant (constants)
N812,
# lowercase imported as non-lowercase
N813,
# camelcase imported as lowercase
N814,
# camelcase imported as constant (distinct from N817 for selective enforcement)
N815,
# mixedCase variable in class scope (constants, method names)
N816,
# mixedCase variable in global scope (constants)
N817,
# camelcase imported as acronym (distinct from N814 for selective enforcement)
N818,
# error suffix in exception names (exceptions)
# -------- https://github.com/PyCQA/flake8-bugbear --------
B001,
# Do not use bare except:, it also catches unexpected events like memory errors, interrupts, system exit, and so on. Prefer except Exception:. If you're sure what you're doing, be explicit and write except BaseException:. Disable E722 to avoid duplicate warnings.
B002,
# Python does not support the unary prefix increment. Writing ++n is equivalent to +(+(n)), which equals n. You meant n += 1.
B003,
# Assigning to os.environ doesn't clear the environment. Subprocesses are going to see outdated variables, in disagreement with the current process. Use os.environ.clear() or the env= argument to Popen.
B004,
# Using hasattr(x, '__call__') to test if x is callable is unreliable. If x implements custom __getattr__ or its __call__ is itself not callable, you might get misleading results. Use callable(x) for consistent results.
B005,
# Using .strip() with multi-character strings is misleading the reader. It looks like stripping a substring. Move your character set to a constant if this is deliberate. Use .replace() or regular expressions to remove string fragments.
B006,
# Do not use mutable data structures for argument defaults. They are created during function definition time. All calls to the function reuse this one instance of that data structure, persisting changes between them.
B007,
# Loop control variable not used within the loop body. If this is intended, start the name with an underscore.
B008,
# Do not perform function calls in argument defaults. The call is performed only once at function definition time. All calls to your function will reuse the result of that definition-time function call. If this is intended, assign the function call to a module-level variable and use that variable as a default value.
B009,
# Do not call getattr(x, 'attr'), instead use normal property access: x.attr. Missing a default to getattr will cause an AttributeError to be raised for non-existent properties. There is no additional safety in using getattr if you know the attribute name ahead of time.
B010,
# Do not call setattr(x, 'attr', val), instead use normal property access: x.attr = val. There is no additional safety in using setattr if you know the attribute name ahead of time.
B011,
# Do not call assert False since python -O removes these calls. Instead callers should raise AssertionError().
B012,
# Use of break, continue or return inside finally blocks will silence exceptions or override return values from the try or except blocks. To silence an exception, do it explicitly in the except block. To properly use a break, continue or return refactor your code so these statements are not in the finally block.
B013,
# A length-one tuple literal is redundant. Write except SomeError: instead of except (SomeError,):.
B014,
# Redundant exception types in except (Exception, TypeError):. Write except Exception:, which catches exactly the same exceptions.
B015,
# Pointless comparison. This comparison does nothing but waste CPU instructions. Either prepend assert or remove it.
B016,
# Cannot raise a literal. Did you intend to return it or raise an Exception?
B017,
# self.assertRaises(Exception): should be considered evil. It can lead to your test passing even if the code being tested is never executed due to a typo. Either assert for a more specific exception (builtin or custom), use assertRaisesRegex, or use the context manager form of assertRaises (with self.assertRaises(Exception) as ex:) with an assertion against the data available in ex.
B018,
# Found useless expression. Either assign it to a variable or remove it.
B019,
# Use of functools.lru_cache or functools.cache on methods can lead to memory leaks. The cache may retain instance references, preventing garbage collection.
B020,
# Loop control variable overrides iterable it iterates
B021,
# f-string used as docstring. This will be interpreted by python as a joined string rather than a docstring.
B022,
# No arguments passed to contextlib.suppress. No exceptions will be suppressed and therefore this context manager is redundant. N.B. this rule currently does not flag suppress calls to avoid potential false positives due to similarly named user-defined functions.
B023,
# Functions defined inside a loop must not use variables redefined in the loop, because late-binding closures are a classic gotcha.
# -------- https://github.com/sbdchd/flake8-pie --------
; PIE781,
# assign-and-return
# Based on Clippy's let_and_return and Microsoft's TSLint rule no-unnecessary-local-variable.
# PIE783,
# celery-explicit-names
# Warn about Celery task definitions that don't have explicit names.
# PIE784,
# celery-explicit-crontab-args
# The crontab class provided by Celery has some default args that are suprising to new users. Specifically, crontab(hour="0,12") won't run a task at midnight and noon, it will run the task at every minute during those two hours. This lint makes that call an error, forcing you to write crontab(hour="0, 12", minute="*").
# PIE785,
# celery-require-tasks-expire
# Celery tasks can bunch up if they don't have expirations.
PIE786,
# precise-exception-handlers
# Be precise in what exceptions you catch. Bare except: handlers, catching BaseException, or catching Exception can lead to unexpected bugs.
PIE787,
# no-len-condition
# Empty collections are fasley in Python so calling len() is unnecessary when checking for emptiness in an if statement/expression.
PIE788,
# no-bool-condition
# If statements/expressions evalute the truthiness of the their test argument, so calling bool() is unnecessary.
PIE789,
# prefer-isinstance-type-compare
# Using type() doesn't take into account subclassess and type checkers won't refine the type, use isinstance instead.
PIE790,
# no-unnecessary-pass
# pass is unnecessary when definining a class or function with an empty body.
PIE791,
# no-pointless-statements
# Comparisions without an assignment or assertion are probably a typo.
# PIE792, # same as SIM120
# no-inherit-object
# Inheriting from object isn't necessary in Python 3.
PIE793,
# prefer-dataclass
# Attempts to find cases where the @dataclass decorator is unintentionally missing.
PIE794,
# dupe-class-field-definitions
# Finds duplicate definitions for the same field, which can occur in large ORM model definitions.
PIE795,
# prefer-stdlib-enums
# Instead of defining various constant properties on a class, use the stdlib enum which typecheckers support for type refinement.
PIE796,
# prefer-unique-enums
# By default the stdlib enum allows multiple field names to map to the same value, this lint requires each enum value be unique.
PIE797,
# no-unnecessary-if-expr
# Call bool() directly rather than reimplementing its functionality.
PIE798,
# no-unnecessary-class
# Instead of using class to namespace functions, use a module.
PIE799,
# prefer-col-init
# Check that values are passed in when collections are created rather than creating an empty collection and then inserting.
PIE800,
# no-unnecessary-spread
# Check for unnecessary dict unpacking.
PIE801,
# prefer-simple-return
# Return boolean expressions directly instead of returning True and False.
PIE802,
# prefer-simple-any-all
# Remove unnecessary comprehensions for any and all
PIE803,
# prefer-logging-interpolation
# Don't format strings before logging. Let logging interpolate arguments.
PIE804,
# no-unnecessary-dict-kwargs
# As long as the keys of the dict are valid Python identifier names, we can safely remove the surrounding dict.
# PIE805,
# prefer-literal
# Currently only checks for byte string literals.
PIE806,
# no-assert-except
# Instead of asserting and catching the exception, use an if statement.
PIE807,
# prefer-list-builtin
# lambda: [] is equivalent to the builtin list
PIE808,
# prefer-simple-range
# We can leave out the first argument to range in some cases since the default start position is 0.
PIE809,
# django-prefer-bulk
# Bulk create multiple objects instead of executing O(N) queries.
# -------- https://github.com/JBKahn/flake8-print --------
T201,
# print found
T203,
# pprint found
T204,
# pprint declared
# -------- https://gitlab.com/RoPP/flake8-use-pathlib --------
PL100,
# os.path.abspath("foo") should be replaced by foo_path.resolve()
PL101,
# os.chmod("foo", 0o444) should be replaced by foo_path.chmod(0o444)
PL102,
# os.mkdir("foo") should be replaced by foo_path.mkdir()
PL103,
# os.makedirs("foo/bar") should be replaced by bar_path.mkdir(parents=True)
PL104,
# os.rename("foo", "bar") should be replaced by foo_path.rename(Path("bar"))
PL105,
# os.replace("foo", "bar") should be replaced by foo_path.replace(Path("bar"))
PL106,
# os.rmdir("foo") should be replaced by foo_path.rmdir()
PL107,
# os.remove("foo") should be replaced by foo_path.unlink()
PL108,
# os.unlink("foo") should be replaced by foo_path.unlink()
PL109,
# os.getcwd() should be replaced by Path.cwd()
PL110,
# os.path.exists("foo") should be replaced by foo_path.exists()
PL111,
# os.path.expanduser("~/foo") should be replaced by foo_path.expanduser()
PL112,
# os.path.isdir("foo") should be replaced by foo_path.is_dir()
PL113,
# os.path.isfile("foo") should be replaced by foo_path.is_file()
PL114,
# os.path.islink("foo") should be replaced by foo_path.is_symlink()
PL115,
# os.readlink("foo") should be replaced by foo_path.readlink()
PL116,
# os.stat("foo") should be replaced by foo_path.stat() or foo_path.owner() or foo_path.group()
PL117,
# os.path.isabs should be replaced by foo_path.is_absolute()
PL118,
# os.path.join("foo", "bar") should be replaced by foo_path / "bar"
PL119,
# os.path.basename("foo/bar") should be replaced by bar_path.name
PL120,
# os.path.dirname("foo/bar") should be replaced by bar_path.parent
PL121,
# os.path.samefile("foo", "bar") should be replaced by foo_path.samefile(bar_path)
PL122,
# os.path.splitext("foo.bar") should be replaced by foo_path.suffix
PL123,
# open("foo") should be replaced by Path("foo").open()
PL124,
# py.path.local is in maintenance mode, use pathlib instead
# -------- https://github.com/c0ntribut0r/flake8-grug --------
GRG001,
# copy-paste
GRG002,
# early quit
GRG003,
# eval
GRG004,
# try too much
GRG005,
# requests no status check
Маленькие открытия, пока я всё это собирал:
Нашёл репо от команды BestDoctor. Они написали несколько плагинов для flake8, плюс вообще у них приятно - рекомендую python styleguide и погулять по репе. BestDoctor, оплату переводите на карту, как и договаривались.
flake8-simplify тоже вполне хорош, я джва года искал набор таких правил
Всякие code quality tools живут в PyCQA, могут пригодиться
Аннигилятор уже изобрели в wemake, но мне не все правила подходили, и у него фатальный недостаток
Когда после всех настроек я запустил аннигилятор, я понял: я создал такого же брюзгу, как я. Ура!
Только посмотрите:
И когда мой мега-линтер начал подсвечивать мой собственный код и бесить меня, я понял: он готов к работе.
Время запускать аннигилятор
Всё началось со статей Хабра, поэтому я решил соскрапать их все и скормить flake8. Я написал простую обёртку над Хабром и оставил на ночь. А потом запустил аннигилятор на всех постах.
1,723 поста про питон без кода или код меньше 5 строчек ¯_(ツ)_/¯
Кодом я считал секции с тегом <code>. Иногда там не код, а командная строка или дурацкое форматирование, так что где flake8 смог переварить - хорошо, где не смог - не обессудьте.
Total posts: 2,791
Overall issues: 63,614
Unique issues (one per post): 12,166
И вот, наконец, долгожданная статистика.
% of posts - процент постов, в которых ошибка встечалась хотя бы один раз
cnt - то же, но в абсолютном значении (количество)
code - код ошибки в Flake8
name - пример текста ошибки, может отличаться от случая к случаю
Ну что, поехали?
Мы прогуляемся по топчику из списка, и ещё я добавлю немного того, что не попало в список, потому что я не запрограммировал или запрограммировать невозможно.
Чтобы не сойти с ума, разобьём всё по секциям, упомянутым выше. Устранив ошибки секции, код можно будет
легко читать
легко изменять
легко отлаживать
легко тестировать
Легко читать
Легко читать > Самодокументация
Лень - двигатель прогресса. Ленивые программисты не любят писать документацию (да и кто вообще любит?), поэтому они стараются писать код, который сам себя документирует и его можно читать как книгу. Бонусом идёт то, что документация всегда в актуальном состоянии.
Легко читать > Самодокументация > Типы
Это питонушка. Тут в переменной может быть всё что угодно, хотите int, хотите гетерогенный список объектов вперемежку со словарями и числами с плавающей точкой. Считаю ли я это преимуществом питона? Нет. В большинстве случаев такая гибкость не нужна, и на этапе написания кода известно, где какие типы, так почему бы это не использовать ? (извините, не удержался от пропаганды)
Но мы имеем то, что имеем:
def fn(items):
# ...
Проблема: а что можно подавать в items, и что возвращается из fn?
Решение 1: смотреть код.
Топорное решение - посмотреть код и понять. И никто не гарантирует успех - может, вы поймёте, что items - это коллекция каких-то объектов, но вот чтобы понять, каких - нужно будет попрыгать по вызовам функции fn и посмотреть, что туда передаётся. Ну такое.
Решение 2: смотреть docstring.
Пещерные люди жили в неведении, но потом открыли для себя докстринги, где можно писать, что делает функция, что получает и что возвращает.
def fn(items):
"""
Sort items using mega-algo.
Args:
items: list - list of items
Returns:
list of sorted elements
"""
# ...
Это почти нормально, если бы не:
разные стили: где-то google-style, где-то sphinx-style
много писать (но помогают плагины)
нужно всё время следить за актуальностью: убрали аргумент, добавили аргумент, поменяли название - будьте добры обновить докстринг (я всё время забываю)
Решение 3: смотреть type hints
Потом пещерные люди посмотрели на статически типизированные языки и внезапно увидели, что это удобно, и захотели так же. Ну хотя бы чтоб было похоже. Поэтому вот:
Смешно, что, хотя для items объявлен тип, по факту вы можете передать туда что угодно. Поэтому всё равно нужно следить, что куда передаётся, но, по крайней мере, это проще, и всякие линтеры могут помогать.
Решение 4: использовать python >= 3.10 и не импортировать лишнего
Наконец, пещерные люди посмотрели на List[Optional[Union[int, float]]], прифигели немного (это ведь только один аргумент!) и решили всё упростить:
def fn(items: list[int | float | None]) -> list[int, float]: # а ещё лучше TypeVar ;)
# ...
Согласитесь, эволюция налицо. Но проблемы всё равно есть:
type hints могут быть слишком большими. Постоянная тема: dict[str, list[dict]] - это что вообще там? Если это тип для аргумента, то название аргумента подскажет, что имел в виду автор. А если это тип возвращаемого значения, то...
return type hint никак не помогает!
Вот посмотрите сами:
def get_orientation(obj: Ufo) -> tuple[float, float, float]: # WAT? это что такое?
# ...
return pitch, roll, yaw # plot twist: я прочитал функцию до конца и узнал, что было в начале
Решение тут пока - в использовании namedtuple или иных заранее определённых типов для возвращаемого значения, но как по мне - слишком много boilerplate кода:
И пока пещерные люди выходят на новый виток эволюции, большинство разработчиков вообще живут мимо эволюции и не используют type annotations - в 80% постов они отсутствуют.
0 80% 2252 ANN Missing type annotation for function argument 'self'
1 77% 2154 TAE001 too few type annotations (0%)
Почему? Я не знаю. Питон 3.5 (а с ним и аннотации типов) вышел 7 лет назад!
Поэтому я напишу это здесь ещё раз: пожалуйста, используйте type hints. Это не сложно (кроме Generator[yield type, send type, return type], они реально бесят).
Легко читать > Самодокументация > Название функции
Ладно, вот вам новая проблема: что делает функция?
Тут сразу понятно: итемсы сортируются по грехам их. Стоп, что?
Ну короче вы поняли. Правильное название функции и короткий, читаемый код заменяют docstring. Признаться, раньше я был свидетелем докстринговым и везде старался их писать, но теперь я их пишу только во всяких публичных функциях публичных либ, чтобы документация красиво собиралась.
Вопрос в кассу: как проверить, что название хорошее? Ответ: люди в чёрном.
Берёте Уилла Смита, он стирает вам память, после чего показывает аннотацию функции. Если вы примерно угадываете, что функция делает - название хорошее.
Легко читать > Самодокументация > Названия переменных
Та же тема с названиями переменных. Дополнительно хочу отметить: никто вас не наругает, если вы используете несколько слов в названии переменной, и она вдруг от этого станет понятной.
Если хорошее название - дело субъективное, то вот переопределение встроенных символов (например, list, set, next) - очевидное зло. Чтобы вы не теряли бдительность, в 5% постов авторы переопределяют встроенные символы при помощи переменных, а в 3% - при помощи аргументов.
5% 150 A001 variable "xxx" is shadowing a python builtin
3% 100 A002 argument "xxx" is shadowing a python builtin
2% 72 CAC002 function is too complex. Bad variable names penalty is too high
Я написал скрипт, который по коду ошибки выдаёт примеры прям из статей с Хабра, так что вот живой пример: какой-то дурачок переопределил repr.
def repr(group_start, group_end) -> str: # <--------------------------------!!!
# это просто правильно печатает группу
if last_group_start == last_group_end:
return str(last_group_end)
Тут всё просто: код рассказывает компьютеру, что делать. Когда человек читает код, он понимает, что компьютер будет делать. Поэтому в большинстве случаев комментировать это не надо. Такие комментарии только занимают место и не приносят никакой пользы.
# делаем проверку по индексу 26
data.loc[26]
Но вот если после Уилла Смита вы не понимаете, что делает этот код (или зачем) - комментарий будет совсем не лишним:
Легко читать > Low RAM
Я уже говорил, что у меня в голове место ограничено, поэтому я стараюсь как можно сильнее снизить когнитивную нагрузку - иначе говоря, минимизировать количество информации, которую мне нужно держать в голове. Для этого я стараюсь уменьшать, разбивать и упрощать функции.
Легко читать > Low RAM > Невысокая функция
Тут всё просто: чем короче функция, тем проще её понять.
Разбитие функции
Функция слишком длинная? Рецепт прост: разбейте её на несколько! Увы, в 16% постов авторы пишут мега-функции:
16% 447 CCR001 Cognitive complexity is too high
12% 359 CAC001 function is too complex
Ну вот, например:
def run(self):
pixels = pygame.surfarray.pixels3d(self.display)
index = 0
running = True
while running:
self.model.stimulate()
pixels[:, :, :] = (255 * (self.model.activity > 0))[:, :, None]
pygame.display.flip()
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_ESCAPE:
running = False
elif event.key == pygame.K_s:
imsave("{0:04d}.png".format(index), pixels[:, :, 0])
index = index + 1
elif event.type == pygame.MOUSEBUTTONDOWN:
position = pygame.mouse.get_pos()
self.model.activity[position] = 1
Тут игровой цикл, где смешались вызов симуляции, отрисовка пикселей, обработка клавиатуры, скриншоты... Как упростить? Разбейте на функции:
def run(self):
while self.is_running:
self.model.simulate()
self.draw()
self.handle_input()
Группировка присваиваний в one-liner
Магическое превращение четырёх строчек в одну! Следите за руками:
if a > b:
max_ = a
else:
max_ = b
# ...вжух!
max_ = a if a > b else b
Я люблю такие присваивания, потому что они занимают меньше места. Но ещё важная фишка в том, что у меня одно присваивание в коде вместо двух, значит, меньше шансов на ошибку.
Легко читать > Low RAM > Неширокая функция
Когда у вас слишком много отступов в коде - это знак, что что-то можно упростить. Например:
Циклы
Если у вас в цикле много всего, то можно это всё вынести в отдельную функцию. Вы не только упрощаете код, но и "сдвигаете" всё из цикла на один уровень вложенности меньше:
def run():
for event in pygame.event.get():
if event.type == pygame.QUIT: # тут уровень отступов == 2
running = False # тут уровень отступов == 3
# ...
# ...вжух!
def process_event(event: Event):
if event.type == pygame.QUIT: # тут уровень отступов == 1
running = False # тут уровень отступов == 2
# ...
def run():
for event in pygame.event.get():
process_event(event)
Мини-бонус: если в цикле вызывается одна функция, то этот цикл очень просто распараллелить (при помощи ThreadPoolExecutor, например).
Длинные выражения
Ну любит народ писать зубодробительные выражения (11%):
11% 325 ECE001 Expression is too complex (13.5 > 7)
Вот, например:
# constraints on communist's or zhulik's votes
def findVariants(s, aim, cnt, suffix):
s.add((r9916['com'] - aim['com'] == # <--------------------------------!!!
Sum([If(Bool('v_%d_%s' % (i, suffix)), rewritten_pecs[i]['increase']['com'], 0) for i in range(19)]) +
Sum([If(Bool('a_%d_%s' % (k, suffix)), 0, approved_pecs[k]['first']['com']) for k in range(53)])))
Я не знаю, что можно сделать с выражением выше, но если у вас какая-то строчка эпической длины, то хотя бы попробуйте разбить её на несколько - так легче читается. В этом очень помогают скобки, ведь то, что нельзя делать в обычном мире, в мире скобок дозволено.
Вот тут скобки позволяют писать + что-то с новой строчки, да ещё и комментарии можно добавлять:
missing_set = ( # <--------------------------------!!!
[(all_letters, '-' * len(all_letters))] * 3 # тут считаем все буквы пропущенными
+ [(all_letters, all_letters)] * 10 # тут считаем все буквы НЕ пропущенными
+ [('aeiouy', '------')] * 30 # тут считаем пропущенными только гласные
)
Или вот простыня текста разрулена скобками:
fragment = (
'Абсолютная идея есть для себя, потому что в ней нет ни перехода, '
'ни предпосылок и вообще никакой определенности, которые не были бы '
'текучи и прозрачны; она есть чистая форма понятия, которая созерцает '
'свое содержание как самое себя. Она есть свое собственное содержание, '
'поскольку она есть идеальное различение самой себя от себя, и одно из '
'этих различий есть тождество с собой, которое, однако, содержит в себе '
'тотальность форм как систему содержательных определений. '
'Это содержание есть система логического.'
f'({author})'
)
Nested conditions
Тут всё просто: зачем два if, если можно один? Вроде банальщина, а тем не менее - 2%:
2% 65 SIM102 Use a single if-statement instead of nested if-statements
if min_val is not None and max_val is not None:
if max_val < min_val:
raise ValueError("max_val is greater than min_val") # тут уровень отступов == 2
# ...вжух!
if min_val is not None and max_val is not None and max_val < min_val:
raise ValueError("max_val is greater than min_val") # тут уровень отступов == 1
Pathlib
pathlib (о котором я замолвлю словечко позже) позволяет читать и писать в одну строчку:
with open('test.txt') as file:
content = open.read() # тут уровень отступов == 1
# ...вжух!
content = Path('test.txt').read_text() # тут уровень отступов == 0
Redundant else
Один из моих любимых способов уменьшения отступов: если в теле после if у вас return или raise, то else не нужен. Проще на примере:
if user_profile:
# ...
return HttpResponseRedirect(reverse("main:dashboard")) # или raise
else:
# ...
messages.success(request, _("Успешная регистрация.")) # тут уровень отступов == 1
return HttpResponseRedirect(reverse("main:dashboard"))
# ...вжух!
if user_profile:
# ...
return HttpResponseRedirect(reverse("main:dashboard"))
# ...
messages.success(request, _("Успешная регистрация.")) # тут уровень отступов == 0
return HttpResponseRedirect(reverse("main:dashboard"))
Легко читать > Low RAM > Небольшой контекст
Каждый if заставляет вас запоминать, что было ветвление на 2 или более случая. Каждая переменная заставляет вас помнить, что она есть и в ней что-то лежит. Каждый try заставляет ожидать исключения в каждой строчке до самого except. Вроде пустяки, но чем их больше, тем сложнее за всем уследить. Всё, что нужно держать в голове, я называю "контекстом", и в этой секции мы будем его уменьшать.
Early quit
Я уже рассказывал про него, но это такой классный паттерн, что я даже добавил его в плагин для flake8 (правда, там много false positives). Правило такое: старайтесь выйти из функции / цикла как можно быстрее. На любой развилке if-else первым проверяйте то условие, которое позволит выйти из тела функции / цикла.
Пример:
if rate is not None:
# в этом месте мы должны помнить, что есть ещё случай `if rate is None`, и его нужно потом рассмотреть
r1 = (df[minus] / df[column] - 1).abs()
r2 = (df[plus] / df[column] - 1).abs()
return df.loc[(df['new_cases'].ge(100)) & ((r1.notnull() & (r1 <= rate)) | (r2.notnull() & (r2 <= rate)))]
else: # и вот мы наконец его рассматриваем
raise Exception
# ...вжух!
if rate is None: # сначала рассматриваем случай, который нас выкинет из текущего контекста
raise Exception
# если не выкинул - то хотя бы не нужно про него больше помнить
r1 = (df[minus] / df[column] - 1).abs()
r2 = (df[plus] / df[column] - 1).abs()
return df.loc[(df['new_cases'].ge(100)) & ((r1.notnull() & (r1 <= rate)) | (r2.notnull() & (r2 <= rate)))]
Обобщая, после этой оптимизации код должен выглядеть так:
если что-то не так:
return
если что-то ещё не так:
return
если ошибка:
raise
# тут мы уверены, что всё в порядке
код, делающий что-то полезное
Small try blocks
В 4% постов авторы пытаются скормить в try как можно больше всего. Наверно, это какие-то корни рыболовов дают о себе знать - натянул сеть пошире и ловишь себе всё, что есть, иногда даже не гнушаясь ловить просто Exception.
4% 106 GRG004 Too big "try" code block
В идеале вы должны знать, что конкретно и где вы ловите - тогда при срабатывании блока except вы точно будете знать, из какой строчки что прилетело. Поэтому в идеале между try и except должно быть 1-3 строчки, а если больше - задумайтесь, возможно, вы не понимаете, что у вас происходит в коде.
Кстати, try-except вводят дополнительный отступ, а чем больше отступов... ну вы помните.
Если у вас прям какая-то ненадёжная функция и вы хотите весь её код завернуть в try-except, то хотя бы юзайте декоратор.
Context manager
7% 208 SIM115 Use context handler for opening files
Внезапно, в 7% постов авторы обходятся без контекстных менеджеров для открытия файлов. Они, наверно, супермены и всегда помнят, что надо закрывать файлы?
@profile
def create_file():
x = [(random.random(),
random_string(),
random.randint(0, 2 ** 64))
for _ in xrange(1000000)]
pickle.dump(x, open('machin.pkl', 'w'))
Я вот могу и забыть про file.close(). Эти ваши интернеты говорят, что если самому не вызвать close(), то cpython закрывает файлы, когда их refcount = 0 и сборщик мусора добирается до них, а вот pypy - только при завершении процесса. Так что лучше использовать контекстные менеджеры, и пусть оно там само помнит, когда что закрыть и отключить:
with open('machin.pkl', 'w') as out_file:
pickle.dump(x, out_file)
Здесь мы рассмотрели контекстный менеджер для open(), но есть и контекстные менеджеры для разного - соединения с базой данных, к примеру - полезные штуки!
Yagni
В коде не должно быть ненужного. Если у вас код под VCS, то смело выбрасывайте мёртвый код - потому что потом можно будет всегда вернуть (но, скорей всего, до этого не дойдёт). Ну и просто по мелочи:
Заменяйте ненужные переменные на underscore
10% 285 B007 Loop control variable 'i' not used within the loop body. If this is intended, start the name with an underscore.
for i in range(32):
print(quantum_randbit(), end='')
# ...вжух!
for _ in range(32):
print(quantum_randbit(), end='')
Для путешестеовенников во времени: у нас в 2022 object уже необязателен.
5% 160 SIM120 Use 'class Bot:' instead of 'class Bot(object):'
class DataItem(object):
__slots__ = ['name', 'age', 'address']
# ...вжух!
class DataItem:
__slots__ = ['name', 'age', 'address']
Не надо создавать контейнер, а потом наполнять его. Наполняйте сразу!
1% 44 PIE799 prefer-col-init: Consider passing values in when creating the collection.
А ещё код легчо читать, если его пишут так, как общепринято и актуально по нынешней спецификации.
Range
Внезапно для 4%, range уже начинается с нуля!
4% 119 PIE808 prefer-simple-range: range starts at 0 by default.
for i in range(0, n):
dydt[i] = y[i + n]
# ...вжух!
for i in range(n):
dydt[i] = y[i + n]
Enumerate
Если в цикле вы используете счётчик и увеличиваете его на 1 в конце каждой итерации, то, скорее всего, доктор вам пропишет enumerate().
2% 70 SIM113 Use enumerate for 'val'
counter = 0
for vk in vk_apis:
change_photo(vk)
print(counter, 'done')
counter += 1
# ...вжух!
for i, vk in enumerate(vk_apis):
change_photo(vk)
print(i, 'done')
Suppress
1% 55 SIM105 Use 'contextlib.suppress(Exception)'
Сам недавно узнал! Позволяет укоротить try-except-pass:
pathlib - это как type hints: полезная штука, которую почему-то часто игнорируют. Если вы используете os.path, то время сесть в делореан, переместиться в будущее и начать пользоваться pathlib.
1% 41 PL107 os.remove("foo") should be replaced by foo_path.unlink()
1% 38 PL103 os.makedirs("foo/bar") should be replaced by bar_path.mkdir(parents=True)
14% 395 PL123 open("foo") should be replaced by Path("foo").open()
3% 96 PL118 os.path.join("foo", "bar") should be replaced by foo_path / "bar"
2% 67 PL110 os.path.exists("foo") should be replaced by foo_path.exists()
os.remove("../m3u8_downloader/segments/temp.ts")
# ...вжух!
Path("../m3u8_downloader/segments/temp.ts").unlink()
if not os.path.exists(tools_path):
os.makedirs(tools_path)
# ...вжух!
tools_path.mkdir(parents=True, exist_ok=True)
with open('posts.txt') as file:
# ...вжух!
with Path('posts.txt').open() as file:
def SaveDriverFile(self):
winPath = os.environ['WINDIR']
sys32Path = os.path.join(winPath, "System32")
targetPath = os.path.join(sys32Path, "drivers\" + self.name + ".sys")
file_data = open(self.file_path, "rb").read()
open(targetPath, "wb").write(file_data)
# ...вжух!
def SaveDriverFile(self):
winPath = Path(os.environ['WINDIR'])
sys32Path = winPath / "System32"
targetPath = sys32Path / "drivers" / self.name + ".sys"
file_data = self.file_path.read_bytes()
targetPath.write_bytes(file_data)
if not os.path.exists(HOME_DIR+'/'+'ShootAndView'):
# ...вжух!
if not (HOME_DIR / 'ShootAndView').exists():
Чтобы не писать зубодробительный код, можно использовать библиотеки, которые этот код уже написали. Тут я опишу лишь те, о которых не могу промолчать.
Requests
Да-да, мы все их используем, но многие не знают двух фишек:
requests.get(...) создаёт новую сессию для каждого вызова, что накладно, если вы делаете запросы к одному и тому же сайту. Правильный подход - использовать сессии, но описано это почему-то в "advanced usage" ¯_(ツ)_/¯
Пожалуйста, не изобретайте велосипеды, если вам нужно запилить retry логику. Используйте tenacity.
More-itertools
Если вы не слышали про itertools из стандартной библиотеки, то самое время почитать. itertools.chain меня постоянно выручает. Но если itertools вам уже не хватает, то вэлкам: more-itertools. Тут есть chunked, spy, first, one, only, unique_everseen и прочие прелести. Осторожно, с этой дряни невозможно слезть.
Легко изменять
Может, вы сами, а может, кто-то из интернета однажды захочет что-то изменить в вашем коде. И этот кто-то мысленно скажет вам спасибо, если вы заранее позаботитесь об этом.
Легко изменять > Классы
Внезапно, классы полезны не только для инкапсуляции состояния объекта, но и для наследования! Вот, например, просто функция:
Как сделать, чтоб функция работала с другой константой? Передавать константу явно в функцию и в каждом месте, где вызывается функция, передавать новую константу. Или определить new_do_something = partial(do_something, constant=6) и везде использовать новую функцию. Но...
Вот то же, но через класс. Теперь кто угодно может переопределить и константу, и функцию, и всё вместе, и надстроить что-то поверх.
class Something:
SOME_CONSTANT = 5
@classmethod
def do_something(cls, value: int) -> int:
return value * cls.SOME_CONSTANT
class Other(Something):
SOME_CONSTANT = 6
@classmethod
def do_something(cls, value: int) -> int:
result = super().do_something(value)
return result - 3
Не то чтобы я топил за использование классов везде - иногда это излишне, и старые добрые функции будут решать задачу без проблем. Но если планируется какое-то переиспользование кода - я бы смотрел в сторону классов.
Легко изменять > Функции
Я прям сразу вспоминаю внутренности Django. Не дай бог мне вдруг не понравится что-то в стандартной инициализации модели - вы только посмотрите, какое там полотно!
base.py (осторожно, опасно для глаз)
class ModelBase(type):
"""Metaclass for all models."""
def __new__(cls, name, bases, attrs, **kwargs):
super_new = super().__new__
# Also ensure initialization is only performed for subclasses of Model
# (excluding Model class itself).
parents = [b for b in bases if isinstance(b, ModelBase)]
if not parents:
return super_new(cls, name, bases, attrs)
# Create the class.
module = attrs.pop("__module__")
new_attrs = {"__module__": module}
classcell = attrs.pop("__classcell__", None)
if classcell is not None:
new_attrs["__classcell__"] = classcell
attr_meta = attrs.pop("Meta", None)
# Pass all attrs without a (Django-specific) contribute_to_class()
# method to type.__new__() so that they're properly initialized
# (i.e. __set_name__()).
contributable_attrs = {}
for obj_name, obj in attrs.items():
if _has_contribute_to_class(obj):
contributable_attrs[obj_name] = obj
else:
new_attrs[obj_name] = obj
new_class = super_new(cls, name, bases, new_attrs, **kwargs)
abstract = getattr(attr_meta, "abstract", False)
meta = attr_meta or getattr(new_class, "Meta", None)
base_meta = getattr(new_class, "_meta", None)
app_label = None
# Look for an application configuration to attach the model to.
app_config = apps.get_containing_app_config(module)
if getattr(meta, "app_label", None) is None:
if app_config is None:
if not abstract:
raise RuntimeError(
"Model class %s.%s doesn't declare an explicit "
"app_label and isn't in an application in "
"INSTALLED_APPS." % (module, name)
)
else:
app_label = app_config.label
new_class.add_to_class("_meta", Options(meta, app_label))
if not abstract:
new_class.add_to_class(
"DoesNotExist",
subclass_exception(
"DoesNotExist",
tuple(
x.DoesNotExist
for x in parents
if hasattr(x, "_meta") and not x._meta.abstract
)
or (ObjectDoesNotExist,),
module,
attached_to=new_class,
),
)
new_class.add_to_class(
"MultipleObjectsReturned",
subclass_exception(
"MultipleObjectsReturned",
tuple(
x.MultipleObjectsReturned
for x in parents
if hasattr(x, "_meta") and not x._meta.abstract
)
or (MultipleObjectsReturned,),
module,
attached_to=new_class,
),
)
if base_meta and not base_meta.abstract:
# Non-abstract child classes inherit some attributes from their
# non-abstract parent (unless an ABC comes before it in the
# method resolution order).
if not hasattr(meta, "ordering"):
new_class._meta.ordering = base_meta.ordering
if not hasattr(meta, "get_latest_by"):
new_class._meta.get_latest_by = base_meta.get_latest_by
is_proxy = new_class._meta.proxy
# If the model is a proxy, ensure that the base class
# hasn't been swapped out.
if is_proxy and base_meta and base_meta.swapped:
raise TypeError(
"%s cannot proxy the swapped model '%s'." % (name, base_meta.swapped)
)
# Add remaining attributes (those with a contribute_to_class() method)
# to the class.
for obj_name, obj in contributable_attrs.items():
new_class.add_to_class(obj_name, obj)
# All the fields of any type declared on this model
new_fields = chain(
new_class._meta.local_fields,
new_class._meta.local_many_to_many,
new_class._meta.private_fields,
)
field_names = {f.name for f in new_fields}
# Basic setup for proxy models.
if is_proxy:
base = None
for parent in [kls for kls in parents if hasattr(kls, "_meta")]:
if parent._meta.abstract:
if parent._meta.fields:
raise TypeError(
"Abstract base class containing model fields not "
"permitted for proxy model '%s'." % name
)
else:
continue
if base is None:
base = parent
elif parent._meta.concrete_model is not base._meta.concrete_model:
raise TypeError(
"Proxy model '%s' has more than one non-abstract model base "
"class." % name
)
if base is None:
raise TypeError(
"Proxy model '%s' has no non-abstract model base class." % name
)
new_class._meta.setup_proxy(base)
new_class._meta.concrete_model = base._meta.concrete_model
else:
new_class._meta.concrete_model = new_class
# Collect the parent links for multi-table inheritance.
parent_links = {}
for base in reversed([new_class] + parents):
# Conceptually equivalent to `if base is Model`.
if not hasattr(base, "_meta"):
continue
# Skip concrete parent classes.
if base != new_class and not base._meta.abstract:
continue
# Locate OneToOneField instances.
for field in base._meta.local_fields:
if isinstance(field, OneToOneField) and field.remote_field.parent_link:
related = resolve_relation(new_class, field.remote_field.model)
parent_links[make_model_tuple(related)] = field
# Track fields inherited from base models.
inherited_attributes = set()
# Do the appropriate setup for any model parents.
for base in new_class.mro():
if base not in parents or not hasattr(base, "_meta"):
# Things without _meta aren't functional models, so they're
# uninteresting parents.
inherited_attributes.update(base.__dict__)
continue
parent_fields = base._meta.local_fields + base._meta.local_many_to_many
if not base._meta.abstract:
# Check for clashes between locally declared fields and those
# on the base classes.
for field in parent_fields:
if field.name in field_names:
raise FieldError(
"Local field %r in class %r clashes with field of "
"the same name from base class %r."
% (
field.name,
name,
base.__name__,
)
)
else:
inherited_attributes.add(field.name)
# Concrete classes...
base = base._meta.concrete_model
base_key = make_model_tuple(base)
if base_key in parent_links:
field = parent_links[base_key]
elif not is_proxy:
attr_name = "%s_ptr" % base._meta.model_name
field = OneToOneField(
base,
on_delete=CASCADE,
name=attr_name,
auto_created=True,
parent_link=True,
)
if attr_name in field_names:
raise FieldError(
"Auto-generated field '%s' in class %r for "
"parent_link to base class %r clashes with "
"declared field of the same name."
% (
attr_name,
name,
base.__name__,
)
)
# Only add the ptr field if it's not already present;
# e.g. migrations will already have it specified
if not hasattr(new_class, attr_name):
new_class.add_to_class(attr_name, field)
else:
field = None
new_class._meta.parents[base] = field
else:
base_parents = base._meta.parents.copy()
# Add fields from abstract base class if it wasn't overridden.
for field in parent_fields:
if (
field.name not in field_names
and field.name not in new_class.__dict__
and field.name not in inherited_attributes
):
new_field = copy.deepcopy(field)
new_class.add_to_class(field.name, new_field)
# Replace parent links defined on this base by the new
# field. It will be appropriately resolved if required.
if field.one_to_one:
for parent, parent_link in base_parents.items():
if field == parent_link:
base_parents[parent] = new_field
# Pass any non-abstract parent classes onto child.
new_class._meta.parents.update(base_parents)
# Inherit private fields (like GenericForeignKey) from the parent
# class
for field in base._meta.private_fields:
if field.name in field_names:
if not base._meta.abstract:
raise FieldError(
"Local field %r in class %r clashes with field of "
"the same name from base class %r."
% (
field.name,
name,
base.__name__,
)
)
else:
field = copy.deepcopy(field)
if not base._meta.abstract:
field.mti_inherited = True
new_class.add_to_class(field.name, field)
# Copy indexes so that index names are unique when models extend an
# abstract model.
new_class._meta.indexes = [
copy.deepcopy(idx) for idx in new_class._meta.indexes
]
if abstract:
# Abstract base models can't be instantiated and don't appear in
# the list of models for an app. We do the final setup for them a
# little differently from normal models.
attr_meta.abstract = False
new_class.Meta = attr_meta
return new_class
new_class._prepare()
new_class._meta.apps.register_model(new_class._meta.app_label, new_class)
return new_class
Чтобы пропатчить одну строчку в этой функции, мне нужно полностью эту функцию переписать! А потом в следующем релизе разрабы джанго там что-то поменяют, и мой патч сломается.
Мораль? Чем меньше метод, тем легче его заменить.
Легко изменять > Переменные
Как я уже говорил выше, если вы хотите, чтобы можно было изменять переменные - то либо делайте их class variables, либо передавайте явно как параметр в функцию. Никаких magic numbers в теле функции!
# бро
class Some:
CONST = 5
# бро
def do(const: int = 5):
# ...
# не бро
def do():
for i in range(5):
# ...
Легко изменять > Dependency injection
Справедливости ради следует упомянуть dependency injection, как вариант, облегчающий модификацию и тестирование кода, но, кажется, за меня про это уже написали тут.
Легко дебажить
Легко дебажить - это значит иметь место для брейкпоинта, и чтоб можно было понять, что вообще происходит.
Легко дебажить > except или except Exception
Я уже упоминал про моряков, которые ловят всё. И это плохо, потому что, отлавливая всё подряд, вы пытаетесь пофиксить своё непонимание - вы не понимаете, какие исключения могут прилететь. Разберитесь в коде, а не пытайтесь поставить заплатку!
6% 171 B001 Do not use bare `except:`, it also catches unexpected events like memory errors, interrupts, system exit, and so on.
9% 274 PIE786 Use precise exception handlers.
for node in G.nodes():
try:
labels[node]=names['keysets']['generated'][nodeStdict[node]+'-name']['ru']
except: labels[node]='error'
# ...вжух!
for node in G.nodes():
try:
labels[node]=names['keysets']['generated'][nodeStdict[node]+'-name']['ru']
except KeyError:
labels[node] = 'error'
Легко дебажить > mega-statements
Чем длиннее выражение, тем сложнее его отлаживать. Например, в этом случае нет места для брейкпоинта, потому что мега-объект создаётся несколько раз в цикле из itertools.product, и сразу с ним делается assert. Это просто ужасно!
Иногда можно писать сразу так, чтобы потом не дебажить :)
Copy-paste
Хотите сделать опечатку? Юзайте copy-paste! За пруфами отсылаю вас к PVS Studio, а на Хабре... на Хабре копи-пасту можно найти очень часто.
44% 715 GRG001 Copy-paste of code
И хотя в моём плагине много false positives, проблема действительно есть:
xn = fincoords[0][:,0]
yn = fincoords[0][:,1]
zn = fincoords[0][:,2]
l=[labels[idi] for idi in myIDs]
Чем больше строчек скопировано, тем легче будет ошибиться - забыть поменять что-то в одной из строчек. Решение - выделять общий функционал в цикл / отдельную функцию.
Response status
Вот вам забавный факт: в 4% постов разработчику вообще плевать, что там вернул сервер. Не стоит прогибаться под изменчивый сервер, пусть лучше он прогнётся под нас!
4% 111 GRG005 Not checking response status code
def uncapcha(url):
imager = requests.get(url)
r = requests.post('http://rucaptcha.com/in.php', data = {'method': 'base64', 'key': RUCAPTCHA_KEY, 'body': base64.b64encode(imager.content)})
if (r.text[:3] != 'OK|'):
print('captcha failed')
return -1
capid = r.text[3:]
sleep(5)
response.ok или response.raise_for_status() в помощь!
Print
Проблема print() в том, что им очень просто пользоваться. Вот все им и пользуются, но через какое-то время возникает другая проблема: а как его заткнуть? Поэтому я рекомендую сразу, прям с самого начала использовать модуль logging и разделять вывод на debug, info, warning и error. Тогда во время отладки программы вы будете выводить все сообщения, даже отладочные, а потом просто смените уровень на info или warning, и отладочные сообщения не будут засорять вам вывод.
Как сделать тестирование лёгким и приятным? А вот как: пишите функции, которые
ни от чего не зависят, кроме входных параметров
делают только одну вещь
без side effects, а если и с ними, то это единственное их назначение
Я собрал джекпот для примера:
items = [1, 2]
def append_double(el: int) -> int:
# функция имеет side effect: изменяет items
double = el * 2
items.append(double) # функция зависит от глобальной переменной items
return double # помимо side effect, функция ещё что-то возвращает
Для всех случаев у меня нет авто-детекторов, но вот статистика по использованию ключевого слова global:
3% 98 W002 Global variable next used
Не делайте так!
def format_time(x, pos=None):
global duration, nframes, k
progress = int(x / float(nframes) * duration * k)
# ...
Время статистики
Давайте применим наше знание, чтобы что-нибудь понять.
Статистика по постам
Рейтинг от общего количества ошибок на пост:
Похоже, чем более грязный код вы пишете, тем меньше у вас шансов получить высокий рейтинг. Но это неточно.
Статистика по популярным питон-библиотекам
Ставил последние версии на 1.09.2022.
django
У меня с django довольно нежные отношения, поэтому мне было интересно: так ли там всё плохо, как мне кажется? Хех, похоже, что да!
Во-первых, джанго кладёт огроменный болт на type annotations. У них так принято
python show-me-sample.py --results results/libs-results-django.json --order random --context 8 ANN
-------------------- samples/libs/django/contrib/postgres/validators.py ---- --------------------
ANN Missing type annotation for function argument 'a'
class RangeMinValueValidator(MinValueValidator):
def compare(self, a, b): # <--------------------------------!!!
return a.lower is None or a.lower < b
-------------------- samples/libs/django/contrib/admin/utils.py ---- --------------------
ANN Missing type annotation for function argument 'obj'
def model_format_dict(obj): # <--------------------------------!!!
"""
Return a `dict` with keys 'verbose_name' and 'verbose_name_plural',
typically for use with string formatting.
`obj` may be a `Model` instance, `Model` subclass, or `QuerySet` instance.
-------------------- samples/libs/django/utils/translation/__init__.py ---- --------------------
CCR001 Cognitive complexity is too high (17 > 7)
def lazy_number(func, resultclass, number=None, **kwargs): # <--------------------------------!!!
if isinstance(number, int):
kwargs["number"] = number
proxy = lazy(func, resultclass)(**kwargs)
else:
original_kwargs = kwargs.copy()
class NumberAwareString(resultclass):
def __bool__(self):
return bool(kwargs["singular"])
def _get_number_value(self, values):
try:
return values[number]
except KeyError:
raise KeyError(
"Your dictionary lacks key '%s'. Please provide "
"it, because it is required to determine whether "
# ...
-------------------- samples/libs/django/db/models/base.py ---- --------------------
CCR001 Cognitive complexity is too high (67 > 7)
class Model(metaclass=ModelBase):
def __init__(self, *args, **kwargs): # <--------------------------------!!!
# Alias some things as locals to avoid repeat global lookups
cls = self.__class__
opts = self._meta
_setattr = setattr
_DEFERRED = DEFERRED
if opts.abstract:
raise TypeError("Abstract models cannot be instantiated.")
pre_init.send(sender=cls, args=args, kwargs=kwargs)
# Set up the storage for instance state
self._state = ModelState()
# There is a rather weird disparity here; if kwargs, it's set, then args
# overrides it. It should be one or the other; don't duplicate the work
# The reason for the kwargs check is that standard iterator passes in by
# args, and instantiation for iteration is 33% faster.
if len(args) > len(opts.concrete_fields):
# Daft, but matches old exception sans the err msg.
raise IndexError("Number of args exceeds number of fields")
if not kwargs:
fields_iter = iter(opts.concrete_fields)
# The ordering of the zip calls matter - zip throws StopIteration
# when an iter throws it. So if the first iter throws it, the second
# is *not* consumed. We rely on this, so don't change the order
# without changing the logic.
for val, field in zip(args, fields_iter):
if val is _DEFERRED:
continue
_setattr(self, field.attname, val)
else:
# Slower, kwargs-ready version.
# ...
Всякие ненужные if-else - тоже есть в наличии, можете брать оптом.
-------------------- samples/libs/django/contrib/auth/views.py ---- --------------------
GRG002 Missing early quit
class LoginView(RedirectURLMixin, FormView):
# ...
def get_default_redirect_url(self):
"""Return the default redirect URL."""
if self.next_page: # <--------------------------------!!!
return resolve_url(self.next_page)
else:
return resolve_url(settings.LOGIN_REDIRECT_URL)
GRG002 Missing early quit
def distinct_sql(self, fields, params):
"""
Return an SQL DISTINCT clause which removes duplicate rows from the
result set. If any fields are given, only check the given fields for
duplicates.
"""
if fields: # <--------------------------------!!!
raise NotSupportedError(
"DISTINCT ON fields is not supported by this database backend"
)
else:
return ["DISTINCT"], []
-------------------- samples/libs/django/utils/cache.py ---- --------------------
GRG002 Missing early quit
def learn_cache_key(request, response, cache_timeout=None, key_prefix=None, cache=None):
"""
Learn what headers to take into account for some request URL from the
response object. Store those headers in a global URL registry so that
later access to that URL will know what headers to take into account
without building the response object itself. The headers are named in the
Vary header of the response, but we want to prevent response generation.
The list of headers to use for cache key generation is stored in the same
cache as the pages themselves. If the cache ages some data out of the
cache, this just means that we have to build the response once to get at
the Vary header and so at the list of headers to use for the cache key.
"""
if key_prefix is None:
key_prefix = settings.CACHE_MIDDLEWARE_KEY_PREFIX
if cache_timeout is None:
cache_timeout = settings.CACHE_MIDDLEWARE_SECONDS
cache_key = _generate_cache_header_key(key_prefix, request)
if cache is None:
cache = caches[settings.CACHE_MIDDLEWARE_ALIAS]
if response.has_header("Vary"): # <--------------------------------!!!
is_accept_language_redundant = settings.USE_I18N
# If i18n is used, the generated cache key will be suffixed with the
# current locale. Adding the raw value of Accept-Language is redundant
# in that case and would result in storing the same content under
# multiple keys in the cache. See #18191 for details.
headerlist = []
for header in cc_delim_re.split(response.headers["Vary"]):
header = header.upper().replace("-", "_")
if header != "ACCEPT_LANGUAGE" or not is_accept_language_redundant:
headerlist.append("HTTP_" + header)
headerlist.sort()
cache.set(cache_key, headerlist, cache_timeout)
return _generate_cache_key(request, request.method, headerlist, key_prefix)
else:
# if there is no Vary header, we still need a cache key
# for the request.build_absolute_uri()
cache.set(cache_key, [], cache_timeout)
return _generate_cache_key(request, request.method, [], key_prefix)
-------------------- samples/libs/django/contrib/postgres/apps.py ---- --------------------
GRG001 Copy-paste of code
if (
not enter
and setting == "INSTALLED_APPS"
and "django.contrib.postgres" not in set(value)
):
connection_created.disconnect(register_type_handlers) # <--------------------------------!!!
CharField._unregister_lookup(Unaccent)
TextField._unregister_lookup(Unaccent)
CharField._unregister_lookup(SearchLookup)
TextField._unregister_lookup(SearchLookup)
CharField._unregister_lookup(TrigramSimilar)
TextField._unregister_lookup(TrigramSimilar)
CharField._unregister_lookup(TrigramWordSimilar)
TextField._unregister_lookup(TrigramWordSimilar)
# Disconnect this receiver until the next time this app is installed
mypy больше всего любит взрывать мозг. Простите за пример:
-------------------- samples/libs/mypy/exprtotype.py ---- --------------------
CCR001 Cognitive complexity is too high (62 > 7)
def expr_to_unanalyzed_type(expr: Expression,
options: Optional[Options] = None,
allow_new_syntax: bool = False,
_parent: Optional[Expression] = None) -> ProperType:
"""Translate an expression to the corresponding type.
The result is not semantically analyzed. It can be UnboundType or TypeList.
Raise TypeTranslationError if the expression cannot represent a type.
If allow_new_syntax is True, allow all type syntax independent of the target
Python version (used in stubs).
"""
# The `parent` parameter is used in recursive calls to provide context for
# understanding whether an CallableArgument is ok.
name: Optional[str] = None
if isinstance(expr, NameExpr):
name = expr.name
if name == 'True':
return RawExpressionType(True, 'builtins.bool', line=expr.line, column=expr.column)
elif name == 'False':
return RawExpressionType(False, 'builtins.bool', line=expr.line, column=expr.column)
else:
return UnboundType(name, line=expr.line, column=expr.column)
elif isinstance(expr, MemberExpr):
fullname = get_member_expr_fullname(expr)
if fullname:
return UnboundType(fullname, line=expr.line, column=expr.column)
else:
raise TypeTranslationError()
elif isinstance(expr, IndexExpr):
base = expr_to_unanalyzed_type(expr.base, options, allow_new_syntax, expr)
if isinstance(base, UnboundType):
if base.args:
raise TypeTranslationError()
if isinstance(expr.index, TupleExpr):
args = expr.index.items
else:
args = [expr.index]
if isinstance(expr.base, RefExpr) and expr.base.fullname in ANNOTATED_TYPE_NAMES:
# TODO: this is not the optimal solution as we are basically getting rid
# of the Annotation definition and only returning the type information,
# losing all the annotations.
return expr_to_unanalyzed_type(args[0], options, allow_new_syntax, expr)
else:
base.args = tuple(expr_to_unanalyzed_type(arg, options, allow_new_syntax, expr)
for arg in args)
if not base.args:
base.empty_tuple_index = True
return base
else:
raise TypeTranslationError()
elif (isinstance(expr, OpExpr)
and expr.op == '|'
and ((options and options.python_version >= (3, 10)) or allow_new_syntax)):
return UnionType([expr_to_unanalyzed_type(expr.left, options, allow_new_syntax),
expr_to_unanalyzed_type(expr.right, options, allow_new_syntax)])
elif isinstance(expr, CallExpr) and isinstance(_parent, ListExpr):
c = expr.callee
names = []
# Go through the dotted member expr chain to get the full arg
# constructor name to look up
while True:
if isinstance(c, NameExpr):
names.append(c.name)
break
elif isinstance(c, MemberExpr):
names.append(c.name)
c = c.expr
else:
raise TypeTranslationError()
arg_const = '.'.join(reversed(names))
# Go through the constructor args to get its name and type.
name = None
default_type = AnyType(TypeOfAny.unannotated)
typ: Type = default_type
for i, arg in enumerate(expr.args):
if expr.arg_names[i] is not None:
if expr.arg_names[i] == "name":
if name is not None:
# Two names
raise TypeTranslationError()
name = _extract_argument_name(arg)
continue
elif expr.arg_names[i] == "type":
if typ is not default_type:
# Two types
raise TypeTranslationError()
typ = expr_to_unanalyzed_type(arg, options, allow_new_syntax, expr)
continue
else:
raise TypeTranslationError()
elif i == 0:
typ = expr_to_unanalyzed_type(arg, options, allow_new_syntax, expr)
elif i == 1:
name = _extract_argument_name(arg)
else:
raise TypeTranslationError()
return CallableArgument(typ, name, arg_const, expr.line, expr.column)
elif isinstance(expr, ListExpr):
return TypeList([expr_to_unanalyzed_type(t, options, allow_new_syntax, expr)
for t in expr.items],
line=expr.line, column=expr.column)
elif isinstance(expr, StrExpr):
return parse_type_string(expr.value, 'builtins.str', expr.line, expr.column,
assume_str_is_unicode=expr.from_python_3)
elif isinstance(expr, BytesExpr):
return parse_type_string(expr.value, 'builtins.bytes', expr.line, expr.column,
assume_str_is_unicode=False)
elif isinstance(expr, UnicodeExpr):
return parse_type_string(expr.value, 'builtins.unicode', expr.line, expr.column,
assume_str_is_unicode=True)
elif isinstance(expr, UnaryExpr):
typ = expr_to_unanalyzed_type(expr.expr, options, allow_new_syntax)
if isinstance(typ, RawExpressionType):
if isinstance(typ.literal_value, int) and expr.op == '-':
typ.literal_value *= -1
return typ
raise TypeTranslationError()
elif isinstance(expr, IntExpr):
return RawExpressionType(expr.value, 'builtins.int', line=expr.line, column=expr.column)
elif isinstance(expr, FloatExpr):
# Floats are not valid parameters for RawExpressionType , so we just
# pass in 'None' for now. We'll report the appropriate error at a later stage.
return RawExpressionType(None, 'builtins.float', line=expr.line, column=expr.column)
elif isinstance(expr, ComplexExpr):
# Same thing as above with complex numbers.
return RawExpressionType(None, 'builtins.complex', line=expr.line, column=expr.column)
elif isinstance(expr, EllipsisExpr):
return EllipsisType(expr.line)
else:
raise TypeTranslationError()
flake8
flake8 оказался очень хорош. Я хотел поглумиться, типа "написали линтер, а сами пишут кое-как", но оказалось ровно наоборот.
-------------------- samples/libs/flake8/utils.py ---- --------------------
CCR001 Cognitive complexity is too high (23 > 7)
def parse_files_to_codes_mapping( # noqa: C901 # <--------------------------------!!!
value_: Union[Sequence[str], str]
) -> List[Tuple[str, List[str]]]:
"""Parse a files-to-codes mapping.
A files-to-codes mapping a sequence of values specified as
`filenames list:codes list ...`. Each of the lists may be separated by
either comma or whitespace tokens.
:param value: String to be parsed and normalized.
"""
if not isinstance(value_, str):
value = "n".join(value_)
else:
value = value_
ret: List[Tuple[str, List[str]]] = []
if not value.strip():
return ret
class State:
seen_sep = True
seen_colon = False
filenames: List[str] = []
codes: List[str] = []
def _reset() -> None:
if State.codes:
for filename in State.filenames:
ret.append((filename, State.codes))
State.seen_sep = True
State.seen_colon = False
State.filenames = []
State.codes = []
requests
Почти хорошо. Не любит type hints, в редких случаях выносит мозг.
scrapy
О, у меня большие подозрения насчёт scrapy. Мне вообще не нравится их подход к скрейпингу, и мне кажется, что там уж точно что-то нечисто! Прав ли я? Конечно, нет! scrapy, как и многие, не уважает type hints и иногда мудрит, но в целом терпим.
pytest
Люблю pytest.
Но он почему-то иногда рыбачит:
PIE786 Use precise exception handlers.
def _should_repr_global_name(obj: object) -> bool:
if callable(obj):
return False
try:
return not hasattr(obj, "__name__")
except Exception: # <--------------------------------!!!
return True
И любит лесенки:
-------------------- samples/libs/_pytest/recwarn.py ---- --------------------
CCR001 Cognitive complexity is too high (25 > 7)
@final
class WarningsChecker(WarningsRecorder):
# ...
def __exit__( # <--------------------------------!!!
self,
exc_type: Optional[Type[BaseException]],
exc_val: Optional[BaseException],
exc_tb: Optional[TracebackType],
) -> None:
super().__exit__(exc_type, exc_val, exc_tb)
__tracebackhide__ = True
# only check if we're not currently handling an exception
if exc_type is None and exc_val is None and exc_tb is None:
if self.expected_warning is not None:
if not any(issubclass(r.category, self.expected_warning) for r in self):
__tracebackhide__ = True
fail(
"DID NOT WARN. No warnings of type {} were emitted. "
"The list of emitted warnings is: {}.".format(
self.expected_warning, [each.message for each in self]
)
)
elif self.match_expr is not None:
for r in self:
if issubclass(r.category, self.expected_warning):
if re.compile(self.match_expr).search(str(r.message)):
break
else:
fail(
"DID NOT WARN. No warnings of type {} matching"
" ('{}') were emitted. The list of emitted warnings"
" is: {}.".format(
self.expected_warning,
self.match_expr,
[each.message for each in self],
# ...
sentry-sdk
Вы уже видите тенденцию? Везде проблемы с аннотациями типов и излишней сложностью:
boto3
По моему опыту, амазон не умеет делать что-то просто. Давайте их разнесём!..
Не аннотировано почти ничего, но всё супер-просто. Вынужден признать, это всё-таки очень неплохой результат.
pandas
Ну что, датасаентисты? У меня даже flake прилично задумался, когда анализировал pandas, и я приготовился к худшему.
Я протестировал все остальные библиотеки, пока готовился pandas, и вот что я увидел:
А ничего! К отсутствию типов мы привыкли, выносом мозга нас не удивишь:
-------------------- samples/libs/pandas/core/window/common.py ---- --------------------
CCR001 Cognitive complexity is too high (70 > 7)
def flex_binary_moment(arg1, arg2, f, pairwise=False): # <--------------------------------!!!
if isinstance(arg1, ABCSeries) and isinstance(arg2, ABCSeries):
X, Y = prep_binary(arg1, arg2)
return f(X, Y)
elif isinstance(arg1, ABCDataFrame):
from pandas import DataFrame
def dataframe_from_int_dict(data, frame_template):
result = DataFrame(data, index=frame_template.index)
if len(result.columns) > 0:
result.columns = frame_template.columns[result.columns]
return result
results = {}
if isinstance(arg2, ABCDataFrame):
if pairwise is False:
if arg1 is arg2:
# special case in order to handle duplicate column names
for i in range(len(arg1.columns)):
results[i] = f(arg1.iloc[:, i], arg2.iloc[:, i])
return dataframe_from_int_dict(results, arg1)
else:
if not arg1.columns.is_unique:
raise ValueError("'arg1' columns are not unique")
if not arg2.columns.is_unique:
raise ValueError("'arg2' columns are not unique")
X, Y = arg1.align(arg2, join="outer")
X, Y = prep_binary(X, Y)
res_columns = arg1.columns.union(arg2.columns)
for col in res_columns:
if col in X and col in Y:
results[col] = f(X[col], Y[col])
return DataFrame(results, index=X.index, columns=res_columns)
elif pairwise is True:
results = defaultdict(dict)
for i in range(len(arg1.columns)):
for j in range(len(arg2.columns)):
if j < i and arg2 is arg1:
# Symmetric case
results[i][j] = results[j][i]
else:
results[i][j] = f(
*prep_binary(arg1.iloc[:, i], arg2.iloc[:, j])
)
from pandas import concat
result_index = arg1.index.union(arg2.index)
if len(result_index):
# construct result frame
result = concat(
[
concat(
[results[i][j] for j in range(len(arg2.columns))],
ignore_index=True,
)
for i in range(len(arg1.columns))
],
ignore_index=True,
axis=1,
)
result.columns = arg1.columns
Конец
В статье могут быть неточности. Я не проверял все 2к статей с 60к ошибок и все исходники библиотек. Но я часто смотрел живые примеры ошибок. И вот что я в итоге понял.
type hints никто не любит
все склонны переусложнять код
А теперь plot twist: почти все примеры ошибок я брал из статей с самым высоким рейтингом. Какой из этого вывод? Его озвучил @economist75:
Статья понравилась. Не кодом, который почти что ужасен, а темой макросов в графических пакетах.
Поэтому, в общем-то, не важно, как вы пишете, важно, что вы пишете. Но если вы будете при этом писать чистый, легко читаемый код - это будет вишенка на торте.